Model a SpringBoot REST API step by step
This guide models a small Customer API from domain types through a provided component boundary. The SpringBoot platform then generates OpenAPI, Java persistence code and Spring MVC endpoints from the same CMN sources.
Use this page as the CMN-first learning sequence: it introduces the model in the order in which you create it and explains how each declaration affects SpringBoot generation. To run one complete HTTP CRUD workflow, continue with Build a CRUD API with the Spring Boot facility. For the full inventory of generated contracts and executable tests in the canonical module, use the Spring Boot example reference.
The complete source is the example-spring-boot module on release/1.3.81. It includes a manually maintained application shell and an HTTP runtime test for the generated CRUD API.
For the underlying syntax, see:
1. Prepare the Maven project
Use Java 21 and Maven 3.9 or newer. The model module needs the generator plugin and the Spring Boot facility:
<properties>
<maven.compiler.release>21</maven.compiler.release>
<joinedworkz.version>1.3.81</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>
<plugin>
<groupId>org.joinedworkz.cmn</groupId>
<artifactId>cmn-maven-plugin</artifactId>
<version>${joinedworkz.version}</version>
<executions>
<execution>
<?m2e ignore?>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<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>
<dependency>
<groupId>org.joinedworkz.facilities</groupId>
<artifactId>spring-boot</artifactId>
<version>${joinedworkz.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>This is only the generation-related part of the POM. A runnable application also needs Spring Boot Web, Jakarta Persistence, MapStruct, the compatible glue contracts referenced by its generated code, a database driver and an application shell. The canonical example uses Genesis as an optional reference implementation; production projects can replace both glue package prefixes as described in Package overrides for helper libraries. Use the example's complete POM or follow Integrate JoinedWorkz into an existing Java project.
2. Model the domain
Create model/customer.cmn:
core package org.joinedworkz.examples.customer
import org.joinedworkz.facilities.common.base
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
enum<integer> SetupType {
NONE: 10
NP: 20
JP: 30
}
type Address {
street*: String(200)
zipCode: String(10)
city*: String(100)
country: String(2)
}
type<entity> Customer {
id**: Id
firstName*: Name
lastName*: Name
email: String(255)
setupType: SetupType
mainAddress: Address
}The important parts are:
type<entity>marksCustomerfor persistence generation;id**is its key;- one
*marks a required field; Addressis a complex value contained byCustomer;SetupTypekeeps the public JSON/OpenAPI names separate from the explicit database codes10,20, and30; andplatform SpringBootactivates SpringBoot plus its Java and Base parents.
core is an optional CMN layer name. In this project it communicates domain ownership and normally becomes the effective layer for outlet routing. It is not a reserved SpringBoot switch, and a project may use a different layer name.
CMN files do not need to follow the Java package directory layout. The package is declared in the file, so the project may arrange model/ by feature.
3. Model the REST resource
Create model/api.cmn:
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 SpringBoot
resource /customers as Customer[] by id {
queryEntities()
createEntity()
readEntity()
updateEntity()
deleteEntity()
}
abstract resource /address as Address { }This model imports the domain and the SpringBoot CRUD method types. The collection resource represents Customer[]; by id selects the key used for instance paths.
The five method types contribute the complete standard CRUD boundary:
| Method type | HTTP contract | Generated target |
|---|---|---|
queryEntities() | GET /customers, HTTP 200 | paginated, sortable and filterable search |
createEntity() | POST /customers, HTTP 201 | data-access create |
readEntity() | GET /customers/{id}, HTTP 200 | data-access read |
updateEntity() | PUT /customers/{id}, HTTP 200 | data-access update |
deleteEntity() | DELETE /customers/{id}, HTTP 204 | data-access delete |
The api layer retains the normal routing behavior and SpringBoot gives it additional meaning: generated controller and API-interface output is explicitly assigned effective layer api, and separate API view types can participate in mapper generation. This example directly uses the imported domain type, so a duplicate external DTO model is not required.
vendorSpecificMimeType=true is a package-level override for resources in this model. It has precedence over the global rest.useVendorSpecificMimeType property. See SpringBoot profile and modeling for the media-type and layer boundaries.
The abstract /address resource is reusable modeling input. Because the component below does not provide it, it is not a standalone endpoint.
4. Provide the API from a component
Create model/customer-backend.cmn:
package org.joinedworkz.examples.backend
import org.joinedworkz.examples.customer.api
import org.joinedworkz.facilities.profiles.springboot
platform SpringBoot
component CustomerBackend
componentNamespace='org.joinedworkz.examples.customer.webapp' {
provide /customers
namespaceSuffix='customers.v1'
controller="CustomerV1Controller" {
// optional pseudo-code can go here
}
}
application CustomerApp {
consists of {
CustomerBackend
}
use {
// external components can be listed here later
}
}The resource declaration defines the HTTP contract; the provide declaration selects the implementation boundary:
CustomerBackendprovides the imported/customersresource;componentNamespaceandnamespaceSuffixdefine the target namespace for component artifacts at this provided boundary;controllernames the generated Spring MVC controller and contributes an OpenAPI tag; and- the optional body can describe behavior for generated diagrams.
The component is also the OpenAPI aggregation boundary. JoinedWorkz writes one model-scoped document for the non-abstract resources in api.cmn and one additional component document containing all endpoints provided by CustomerBackend, even when they originate in several imported resource models.
CustomerApp composes components for architecture diagrams. It does not add a third application-wide OpenAPI document.
5. Select the naming flavor
The canonical example explicitly selects the modern SpringBoot naming strategies:
platform.springboot.flavor=modern
cartridge.IntegrationTestCartridge.enabled=falseThe default is legacy, retained for compatibility. modern uses corrected reference-field capitalization, singular table names and escaping for known reserved table and column names. Select the flavor before accepting generated Java or database history. A later switch can change DTO field names and persistence identifiers and therefore requires a clean regeneration, API review and explicit database migration.
The second property explicitly retains the profile default: the generated integration-test cartridge is opt-in. The executable contract of this example is the complete manual runtime test described below. Projects that set cartridge.IntegrationTestCartridge.enabled=true receive only eligible flat Create-to-Read tests; update, delete and query remain manual test responsibilities.
6. Generate and inspect the result
Run generation and compilation from the example module:
mvn clean verifyFor a faster generation-only cycle:
mvn clean generate-sourcesThe example produces these replaceable Java classes under src/generated/java:
CustomerJakarta entity;AddressDtoandCustomerDto;SetupType;SetupTypeAttributeConverter;CustomerMapper;CustomerRepository;CustomerDataAccessService;CustomerV1Api; andCustomerV1Controller.
MapStruct writes the mapper implementation during Java compilation. The runtime call chain is:
HTTP request
-> CustomerV1Controller
-> CustomerDataAccessService
-> CustomerMapper
-> CustomerRepository
-> databaseResponses return through the mapper and controller in the opposite direction. Query requests additionally pass through the query-specification parser and query service supplied by the configured glue implementation.
Base generation also produces:
src/generated/resources/openapi/org.joinedworkz.examples.customer.api.yaml;src/generated/resources/openapi/org.joinedworkz.examples.backend_customerbackend.yaml;- matching viewers below
diagram/api; and - type, component, application and operation diagrams below
diagram.
Treat src/generated/** and the declared replaceable diagram tree as generator-owned. Change CMN or configuration and regenerate; do not patch the derived Java or OpenAPI files. See Generated output, ownership and regeneration.
7. Run the generated API
Generation does not create an application shell. The example supplies the manual SpringBootExampleApplication, runtime dependencies and an H2 development configuration. Start its packaged application:
mvn clean package
java -jar target/example-spring-boot-1.3.81.jarThe API is then available at http://localhost:8080/customers. The Customer model deliberately marks the UUID as a key without choosing a generation strategy, so the create request continues to supply the assigned identifier. SpringBoot's separate generation choices are documented under ID generation strategies.
The example's manual CustomerCrudRuntimeTest starts Spring Boot on a random port and exercises, through real HTTP calls:
- create and read with public enum name
NP, including a direct H2 check for database code20; - creation of a second customer;
- sorted query with response counts;
- update to public enum name
JP, including a direct H2 check for database code30, and read-back; - delete; and
- query after deletion.
That test crosses JSON serialization, the generated controller, query handling, MapStruct, JPA and H2. Run it with mvn clean verify. It is separate from the generated integration-test outlet and remains manual project source.
For the complete request bodies and curl commands, continue with Build a CRUD API with Spring Boot.
8. Adapt the structure
When transferring the pattern to another project:
- keep the entity, API and component concerns in clear model packages;
- choose and record one SpringBoot flavor;
- use any project-specific layer names, but reserve
apiwhen the SpringBoot API-routing behavior is intended; - route generated output only to declared replaceable directories;
- add every effective generated source/resource directory to its consuming Maven module;
- provide a manual application shell, dependencies and runtime configuration;
- clean, regenerate, compile and run consumer-level tests; and
- configure Flyway only after reading Spring Boot: Flyway schema migrations.
For a multi-module layout, use Multi-module setup with outlet overrides.
9. Other method types
The SpringBoot entity CRUD types are the intended choice for the generated DataAccessService flow above. Base also supplies platform-neutral opinionated types such as create, read, update, deleteInstance, query and list, plus raw HTTP types:
methodtype get GET
methodtype post POST
methodtype put PUT
methodtype patch PATCH
methodtype delete DELETEUse raw types when the predefined semantics do not match your endpoint. Define the representations, status codes and instance/collection behavior explicitly. Raw Base methods do not automatically select the SpringBoot entity data-access handlers.
