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:
The canonical Customer source for release 1.3.80 is the example-spring-boot module on release/1.3.80. Its generation, main-source compilation and packaging were verified at commit df7cabf7f21b with Java 21.0.8 and Maven 3.9.16 on 2026-07-26. The generated integration test was not registered, compiled or run; runtime CRUD smoke testing is a separate pending gate.
1. Prerequisites
Before you start, make sure you have:
- Java 21 installed
- Maven 3.9 or newer (
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):
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<joinedworkz.version>1.3.80</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>This excerpt shows the JoinedWorkz and generated-source setup. Use the canonical module's complete POM when compiling the release example, including its Spring Boot, persistence and mapper dependencies and application shell.
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:
core package org.joinedworkz.examples.customer
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.profiles.springboot
platform SpringBootcore– optional layer; in release 1.3.80, tag-aware Java generators can use it to select a source-tag-specific outlet directory.package– namespace of your model.import– Base 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: Base types and the Base profile have global default imports. The explicit imports used by the canonical source keep the model's dependencies visible.
2.1 Enum for setup type
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
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
type<entity> Customer {
id**: Id
firstName*: Name
lastName*: Name
email: String(255)
setupType: SetupType
mainAddress: Address
}<entity>– stereotypesCustomeras an entity.id**– marks the field as the entity/resource key.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 diagram generation by the Base platform (via SpringBoot).
3. Model the REST API in a separate file
The release example keeps the API in a separate model file. The layer (core, api, …) expresses architectural intent and can route generated Java classes through a matching source-tag mapping. Other outlets require generator-specific verification; the Base OpenAPI generators in release 1.3.80 use only their global outlet directory. For example:
model/api.cmn
Header:
api package org.joinedworkz.examples.customer.api vendorSpecificMimeType=true
import org.joinedworkz.examples.customer
import org.joinedworkz.facilities.profiles.springboot
import org.joinedworkz.facilities.springboot.api
platform SpringBootapi– layer for API-related models.import org.joinedworkz.examples.customer– brings the domain types (Customer,Address,SetupType, …) into scope.org.joinedworkz.facilities.springboot.apiprovides the entity CRUD method types used below.- The SpringBoot platform is again selected so that OpenAPI and Spring artefacts are generated for this file.
3.1 Collection resource /customers
resource /customers as Customer[] by id {
queryEntities()
createEntity()
readEntity()
updateEntity()
deleteEntity()
}What this says:
/customersis a collection resource ofCustomer(Customer[]).- Items are identified by the
idfield ofCustomer. - We support:
queryEntities()for a paginated, sortable and filterable querycreateEntity(),readEntity()andupdateEntity()for entity CRUDdeleteEntity()for deletion
These method types come from the imported SpringBoot API model. They define the HTTP verbs, representations, success codes, CRUD semantics and generated CustomerDataAccessService handler targets used by the release example.
Pagination, sorting and filtering metadata are supplied by
queryEntities(). They do not need to be repeated as explicit resource method parameters.
3.2 Reusable address resource
The canonical API model also defines an abstract resource for an address:
abstract resource /address as Address { }An abstract resource is reusable modeling input and is not emitted as a standalone endpoint. The release example deliberately does not attach or provide /address; doing so would be a separate extension with its own verification.
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:
package org.joinedworkz.examples.backend
import org.joinedworkz.examples.customer.api
import org.joinedworkz.facilities.profiles.springboot
platform SpringBootThe canonical component model is untagged. The key point is that the platform is again SpringBoot – this is what enables generation of Spring controllers and related artefacts. If you selected only the Base platform here, you would get OpenAPI and diagrams but no Java controllers.
4.1 Component: CustomerBackend
component CustomerBackend
basePackage='org.joinedworkz.examples.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 org.joinedworkz.examples.customer.webapp.customers.v1 based on this information.
4.2 Application: CustomerApp
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 application/component diagrams.
The Base OpenAPI cartridge uses the component, not the application, as the aggregation boundary. In release 1.3.80 it generates:
- a model-scoped document containing the non-abstract resources declared in the API model;
- an additional component-scoped document containing every endpoint in
CustomerBackend'sprovidedeclarations.
This component document can combine resources imported from several CMN models. Controller names can be used as operation tags. The application does not produce a third, application-wide OpenAPI document.
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:
mvn clean verifyor during development:
mvn generate-sourcesFor the canonical release module, the verified default outputs include:
- Model-scoped OpenAPI YAML
src/generated/resources/openapi/org.joinedworkz.examples.customer.api.yaml - Aggregated component OpenAPI YAML
src/generated/resources/openapi/org.joinedworkz.examples.backend_customerbackend.yaml - Matching OpenAPI HTML viewers
diagram/api/org.joinedworkz.examples.customer.api.htmlanddiagram/api/org.joinedworkz.examples.backend_customerbackend.html - Controller and API interface
src/generated/java/org/joinedworkz/examples/customer/webapp/customers/v1/controller/CustomerV1Controller.javaandsrc/generated/java/org/joinedworkz/examples/customer/webapp/customers/v1/api/CustomerV1Api.java
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 other method types (advanced)
This guide uses the SpringBoot entity CRUD method types createEntity(), readEntity(), updateEntity(), queryEntities() and deleteEntity(). They are the canonical choice for the generated DataAccessService pattern shown here.
The Base facility additionally provides platform-neutral opinionated method types such as create(), read(), update(), query() and list(). It also provides raw method types for lower-level HTTP contracts.
If you need very special HTTP contracts or non-standard behaviour, the Base facility also provides raw method types without defaults:
methodtype get GET
methodtype post POST
methodtype put PUT
methodtype patch PATCH
methodtype delete DELETERelease 1.3.80 also contains a second Base declaration named delete. Therefore, use the unambiguous get, post, put, and patch names where appropriate, but define a uniquely named project method type such as removeCustomer for DELETE rather than relying on Base delete() defaults. You can then specify 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.
