Skip to content

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:

xml
<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:

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> marks Customer for persistence generation;
  • id** is its key;
  • one * marks a required field;
  • Address is a complex value contained by Customer;
  • SetupType keeps the public JSON/OpenAPI names separate from the explicit database codes 10, 20, and 30; and
  • platform SpringBoot activates 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:

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 typeHTTP contractGenerated target
queryEntities()GET /customers, HTTP 200paginated, sortable and filterable search
createEntity()POST /customers, HTTP 201data-access create
readEntity()GET /customers/{id}, HTTP 200data-access read
updateEntity()PUT /customers/{id}, HTTP 200data-access update
deleteEntity()DELETE /customers/{id}, HTTP 204data-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:

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:

  • CustomerBackend provides the imported /customers resource;
  • componentNamespace and namespaceSuffix define the target namespace for component artifacts at this provided boundary;
  • controller names 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:

properties
platform.springboot.flavor=modern
cartridge.IntegrationTestCartridge.enabled=false

The 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:

bash
mvn clean verify

For a faster generation-only cycle:

bash
mvn clean generate-sources

The example produces these replaceable Java classes under src/generated/java:

  • Customer Jakarta entity;
  • AddressDto and CustomerDto;
  • SetupType;
  • SetupTypeAttributeConverter;
  • CustomerMapper;
  • CustomerRepository;
  • CustomerDataAccessService;
  • CustomerV1Api; and
  • CustomerV1Controller.

MapStruct writes the mapper implementation during Java compilation. The runtime call chain is:

text
HTTP request
  -> CustomerV1Controller
  -> CustomerDataAccessService
  -> CustomerMapper
  -> CustomerRepository
  -> database

Responses 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:

bash
mvn clean package
java -jar target/example-spring-boot-1.3.81.jar

The 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:

  1. create and read with public enum name NP, including a direct H2 check for database code 20;
  2. creation of a second customer;
  3. sorted query with response counts;
  4. update to public enum name JP, including a direct H2 check for database code 30, and read-back;
  5. delete; and
  6. 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:

  1. keep the entity, API and component concerns in clear model packages;
  2. choose and record one SpringBoot flavor;
  3. use any project-specific layer names, but reserve api when the SpringBoot API-routing behavior is intended;
  4. route generated output only to declared replaceable directories;
  5. add every effective generated source/resource directory to its consuming Maven module;
  6. provide a manual application shell, dependencies and runtime configuration;
  7. clean, regenerate, compile and run consumer-level tests; and
  8. 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:

cmn
methodtype get GET
methodtype post POST
methodtype put PUT
methodtype patch PATCH
methodtype delete DELETE

Use 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.