Skip to content

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 -v should 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 SpringBoot
  • core – optional layer used for outlet routing (can later be used in joinedworkz.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 for maxLength=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> – stereotypes Customer as an entity.
  • id** – id field with key stereotype.
  • firstName*, lastName* – mandatory fields.
  • Address is 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 SpringBoot
  • api – 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:

  • /customers is a collection resource of Customer (Customer[]).
  • Items are identified by the id field of Customer.
  • We support:
    • query with filters and pagination
    • standard create, read, update, delete operations

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.

page is 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 SpringBoot

You 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 /customers resource 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 package

or during development:

bash
mvn generate-sources

Where 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 to basePackage and subPackage)

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:

  1. Import the Maven project.
  2. Open the .cmn models in the editor.
  3. Fix any validation errors reported in the Problems view.
  4. Save the files – this can trigger generation.
  5. 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 DELETE

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