Appearance
Model a REST API step by step
This guide walks you through modelling a small REST API with JoinedWorkz – from domain model to resources, component and application – and generating OpenAPI (and optionally Spring Boot artefacts).
We’ll use a simple Customer domain as running example and the SpringBoot platform so that the same model can drive both OpenAPI and Spring Web controllers.
For background reading see:
You can also look at the public example project:
- JoinedWorkz quickstart repository:
https://gitlab.com/joinedworkz/joinedworkz-quickstart
1. Prerequisites
Before you start, make sure you have:
- Java 17 installed
- Maven installed (
mvn -vshould work) - JoinedWorkz Studio installed (optional but recommended)
- A project that has:
- the JoinedWorkz Maven plugin configured
- a dependency to the SpringBoot facility
A minimal POM setup looks like this (simplified):
xml
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<joinedworkz.version>1.3.74</joinedworkz.version>
</properties>
<build>
<resources>
<resource>
<directory>model</directory>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
<resource>
<directory>src/generated/resources</directory>
</resource>
</resources>
<plugins>
<!-- JoinedWorkz generator -->
<plugin>
<groupId>org.joinedworkz.cmn</groupId>
<artifactId>cmn-maven-plugin</artifactId>
<version>${joinedworkz.version}</version>
<executions>
<execution>
<?m2e ignore?><!-- ignore this execution in Eclipse -->
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- register generated Java sources (if you generate Java) -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>add-generated-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>${basedir}/src/generated/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Spring Boot platform (includes Java and Base via transitive dependencies) -->
<dependency>
<groupId>org.joinedworkz.facilities</groupId>
<artifactId>spring-boot</artifactId>
<version>${joinedworkz.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>If you prefer, you can clone the quickstart project and use it as a base:
git clone https://gitlab.com/joinedworkz/joinedworkz-quickstart.git
2. Create the domain model
Create a model file, for example:
model/customer.cmn
Unlike Java, CMN files do not need to live in a directory structure that matches the package name. You are free to organise the
model/folder by feature (e.g.common,core,api, …).
Add a header with layer, package, imports and platform:
cmn
core package com.example.customer
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.common.base.api
import org.joinedworkz.facilities.profiles.spring.boot
platform SpringBootcore– optional layer used for outlet routing (can later be used injoinedworkz.properties).package– namespace of your model.import– Base types and method types from the Base facility, plus the SpringBoot profile.platform SpringBoot– tells JoinedWorkz to interpret the model with the SpringBoot platform (which builds on Java and Base).
Note: Some Base packages are imported automatically by the platform (e.g.
org.joinedworkz.facilities.common.base), but using explicit imports keeps the model self-explanatory.
2.1 Enum for setup type
cmn
enum SetupType {
NONE value="none"
NP value="NP"
JP value="JP"
}Each enum literal gets a value property that can be used for external codes (e.g. in JSON or databases).
2.2 Address type
cmn
type Address {
street*: String(200)
zipCode: String(10)
city*: String(100)
country: String(2)
}*marks mandatory fields.String(200)is a shorthand formaxLength=200(see simple types).
2.3 Customer entity
cmn
type<entity> Customer {
id**: Id
firstName*: Name
lastName*: Name
email: String(255)
setupType: SetupType
mainAddress: Address
}<entity>– stereotypesCustomeras an entity.id**– id field with key stereotype.firstName*,lastName*– mandatory fields.Addressis embedded as a containment (default:relationship).
At this point you have a valid domain model that can already be used for schema and diagram generation by the Base platform (via SpringBoot).
3. Model the REST API in a separate file
It is often useful to keep the API in a separate model file so that you can route outlets differently per layer (core, api, …). For example:
model/customer-api.cmn
Header:
cmn
api package com.example.customer.api
import com.example.customer
import org.joinedworkz.facilities.common.base.api
import org.joinedworkz.facilities.profiles.spring.boot
platform SpringBootapi– layer for API-related models.import com.example.customer– brings the domain types (Customer,Address,SetupType, …) into scope.- The SpringBoot platform is again selected so that OpenAPI and Spring artefacts are generated for this file.
3.1 Collection resource /customers
cmn
resource /customers as Customer[] by id {
query(category: String, ^page: Integer, pageSize: Integer)
create() consumes=Customer
read readCustomer()
update()
delete()
}What this says:
/customersis a collection resource ofCustomer(Customer[]).- Items are identified by the
idfield ofCustomer. - We support:
querywith filters and pagination- standard
create,read,update,deleteoperations
The method names (query, create, read, update, delete) refer to method types from the imported BaseApi.cmn model. These method types define the HTTP verbs, consumes/produces defaults, success codes, etc.
create() consumes=Customer overrides the default request body type for create to the full Customer representation. You could also use a separate NewCustomer DTO if you prefer.
pageis currently a reserved name in the UX modelling context. Prefixing it with^(^page) escapes it so it can be used as a normal parameter name. Other reserved names can be escaped in the same way.
3.2 Nested resource /customers/{id}/address
We add a nested resource for the customer address. First define the abstract resource (can be in the same file or a separate shared API file):
cmn
abstract resource /address as Address { }Then reference it as an item-level sub-resource under /customers:
cmn
resource /customers as Customer[] by id {
query(category: String, ^page: Integer, pageSize: Integer)
create() consumes=Customer
read readCustomer()
update()
delete()
./address
}./address means: for each /customers/{id} item there is a nested /customers/{id}/address resource that is defined by the abstract resource.
The /address resource is a singleton sub-resource under an already identified customer: once /customers/{id} has selected a concrete customer, /customers/{id}/address refers to “the address of that customer”. Because the address is always reached relative to the customer item, it does not define its own identifier.
You can now add methods (e.g. read, update) to the /address resource if needed.
4. Attach the API to a component and application
To tie the API to an implementation context, you model a component and an application in a third file, for example:
model/customer-backend.cmn
Header:
cmn
backend package com.example.customer.backend
import com.example.customer.api
import org.joinedworkz.facilities.profiles.spring.boot
platform SpringBootYou could use any layer name here; backend is just one option. The key point is that the platform is again SpringBoot – this is what enables generation of Spring controllers and related artefacts. If you would select only the Base platform here, you would get OpenAPI but no controllers.
4.1 Component: CustomerBackend
cmn
component CustomerBackend
basePackage='com.example.customer.webapp' {
provide /customers
subPackage='customers.v1'
controller="CustomerV1Controller" {
// optional pseudo-code can go here
}
}component– defines a technical building block.provide /customers– this component implements the/customersresource from the API model.basePackage+subPackage– used for generated Java package names.controller– logical controller name, used as tag/class name depending on the platform.
When the SpringBoot platform is active, it can generate controller classes in com.example.customer.webapp.customers.v1 based on this information.
4.2 Application: CustomerApp
cmn
application CustomerApp {
consists of {
CustomerBackend
}
use {
// external components can be listed here later
}
}The application:
- gives you a high-level view of the components that belong together
- allows the Base platform to generate an aggregated OpenAPI document and diagrams for the whole application.
5. Generate the artefacts
You can now generate OpenAPI (and optionally Java / Spring Boot artefacts) either via Maven or via JoinedWorkz Studio.
5.1 Using Maven
From the project root, run:
bash
mvn clean packageor during development:
bash
mvn generate-sourcesWhere to look for generated artefacts (assuming default outlets from the Base and SpringBoot facilities, and no custom overrides):
- OpenAPI YAML
src/generated/resources/openapi/com.example.customer.backend_customerbackend.yaml - OpenAPI HTML viewer
diagram/api/...(depending on your outlet configuration) - Spring Boot controllers / Java sources (if enabled by the platform)
src/generated/java/...(packages according tobasePackageandsubPackage)
Make sure the src/generated/resources and src/generated/java directories are registered in your POM as shown in the prerequisites section.
If you route outlets to other modules via joinedworkz.properties, look for the generated artefacts in the corresponding target modules.
5.2 Using JoinedWorkz Studio
If you open the project in JoinedWorkz Studio:
- Import the Maven project.
- Open the
.cmnmodels in the editor. - Fix any validation errors reported in the Problems view.
- Save the files – this can trigger generation.
- Alternatively, use the explicit Generate command from the menu or context menu.
Generation progress and any generator messages are shown in a dedicated console. Errors and warnings are linked back to the model elements in the editor.
6. Next steps
From here you can extend the example in several directions:
Error modelling
Define error types (e.g.NotFoundError,ValidationError) and add them as additional responses to method types or resource methods.Versioning
Introduce versioned subpackages (e.g.customers.v2) and map them to separate controllers in the component model.DTOs and projections
Use separate DTO types (<projection>or plain complex types) for external representations, and map from entities to DTOs in generators.Integration with existing Spring Boot projects
Use the generated OpenAPI or controllers as starting point and integrate them into an existing codebase.
For more details on the individual modelling constructs, refer back to:
7. Using raw method types (advanced)
In this guide we used the opinionated method types from the Base facility:
cmn
create(), read(), update(), delete(), query(), list()These method types already define HTTP method, status codes and default consumes / produces behaviour. For most REST-style APIs this is the recommended approach, because it keeps the model compact and consistent.
If you need very special HTTP contracts or non-standard behaviour, the Base facility also provides raw method types without defaults:
cnm
methodtype get GET
pethodtype post POST"
methodtype put PUT
methodtype patch PATCH
methodtype delete DELETEYou can reference these raw method types in your resources instead of create / read / update / delete / query / list, and then specify all details directly on the resource methods:
consumes/produces(including dictionaries)- success and error status codes
- additional responses
- whether the method operates on the collection or a resource item
This gives you full freedom when you need it, while the opinionated method types remain the primary building blocks for typical REST APIs.
