Skip to Content
Genexy IDE Beta 0.9.1 · Download the Beta build
Features · Code generation

Spring Boot (`spring-boot`)

PROIDEExperimental

Generates a Spring Boot service in Java or Kotlin. It builds with Gradle (Kotlin DSL, the default) or Maven, always through the wrapper. The app project holds your code. Every API contract and every database is a module of its own. The platform module has error handling, ports, middleware and security.

The generated code uses Spring MVC, Bean Validation, JPA (Hibernate) or JdbcClient, HikariCP and Flyway. It does not use Spring Security: tokens are verified in the platform with JDK cryptography. This gives Java and Kotlin, Gradle and Maven, and Spring Boot 3.5 and 4.x the same code, with no extra dependency. All versions are pinned in one place: gradle/libs.versions.toml with Gradle, or the root pom.xml (on spring-boot-starter-parent) with Maven.

Quick start

gnx create orders --type spring-boot --dir services/orders --language kotlin --build-tool maven --contract services/orders/api/orders.openapi.yaml --port 8080

--type spring-boot-kotlin is a shortcut for --language kotlin. The component spec:

# services/orders/.genexy/component.yaml type: spring-boot language: kotlin buildTool: maven groupId: com.acme packageName: com.acme.orders javaVersion: '21' springBootVersion: 4.1.1 exposes: - name: api port: 8080 contracts: - $ref: api/orders.openapi.yaml modules: - name: main-db type: postgresql-adapter driver: jdbc contract: { $ref: db/orders.yaml }

Component spec

Optiongnx create flagDefaultNotes
language--languagejavajava or kotlin
buildTool--build-toolgradlegradle or maven. Both produce the same modules, sources and tests
mavenVersion3.9.11The Maven the wrapper downloads (buildTool: maven)
groupId--groupcom.example
artifactIdthe component id
packageName--package<groupId>.<artifactId>Base package of the generated code
javaVersion--java2117 or newer
springBootVersion--boot4.1.14.x, or 3.4 and newer
name, description, versionthe id, –, 1.0.0
basePathfrom the contractPath prefix of every contract; otherwise taken from servers[0].url
dependencies, testDependenciesExtra libraries of app/ as group:artifact[:version]
exposes--port, --contractone port, 8080Ports: see Ports, Middleware & Security
exposes[].middleware, .validation, .securityMiddleware, validation, authentication
kind, parts--parentserviceA part of a service in its own folder, see Parts
modulesDatabases and security modules, see below

Modules

TypeContractWhat you get
postgresql-adaptersqlschemamodules/db-<name>: rows, repositories, queries, transactions, Flyway migrations
mysql-adapter (also mariadb-adapter)sqlschemaThe same for MySQL and MariaDB
securityJWT / OIDC bearer tokens for the ports that use it. See Authentication

Database modules take driver: jpa (default) or driver: jdbc. With several databases, mark one primary: true. REST clients, Redis and messaging modules are not generated for Spring Boot yet. The generator skips them with a warning.

What is generated

orders/ ├─ .genexy/component.yaml the component spec (and generated.json) ├─ settings.gradle.kts, build.gradle.kts Gradle: generated ├─ gradlew, gradlew.bat, gradle/ Gradle: wrapper and libs.versions.toml ├─ build-logic/ Gradle: shared conventions ├─ pom.xml, mvnw, mvnw.cmd, .mvn/ Maven (instead of the Gradle files): root pom and wrapper ├─ compose.yaml generated with a database ├─ Dockerfile, README.md, gradle.properties yours (created once) ├─ app/ │ ├─ build.gradle.kts | pom.xml generated │ └─ src/ │ ├─ main/kotlin/com/acme/orders/ │ │ ├─ OrdersApplication.kt generated │ │ ├─ ports/ApiPort.kt generated: one configuration per port │ │ └─ orders/OrdersServiceImpl.kt stub, one per OpenAPI tag │ ├─ main/resources/ │ │ ├─ application.yml generated: the values of the spec │ │ └─ config/application.yml yours: your settings │ └─ test/kotlin/com/acme/orders/ │ ├─ ArchitectureTests.kt, PortsTests.kt, TestDatabases.kt generated │ └─ OrdersApplicationTests.kt stub └─ modules/ generated, never edit ├─ api-orders/ <Tag>Service interfaces, model/, internal/ controllers ├─ db-main-db/ repositories, queries, transactions, db/migration/main-db/V<n>__*.sql └─ platform/ ApiException, UploadedFile, Principal, CurrentPrincipal, internal/ runtime

With Java the sources are under src/main/java and end in .java; models are records in Java and data classes in Kotlin.

The public packages for your code:

PackageTypes
<package>.api.<contract><Tag>Service and model.* of each API contract
<package>.db.<database><Row>Repository, <Db>Queries, <Db>Transactions, @<Db>Transactional, model.* rows
<package>.platformApiException, UploadedFile, Principal, CurrentPrincipal, DataConflictException, RowNotFoundException, PortRegistry

Code in app/ must not use the .internal packages of the modules, Spring Web or JPA directly. The generated ArchUnit test ArchitectureTests fails if it does.

Writing the business logic

There is one <Tag>ServiceImpl per OpenAPI tag, a @Service that implements the generated interface. The interface has default methods that answer 501, so the service builds and runs right away, and operations added to the contract later answer 501 until you implement them.

package com.acme.orders.orders import com.acme.orders.api.orders.OrdersService import com.acme.orders.api.orders.model.Order import com.acme.orders.db.maindb.MainDbTransactions import com.acme.orders.db.maindb.OrderRepository import com.acme.orders.platform.ApiException import com.acme.orders.platform.CurrentPrincipal import org.springframework.stereotype.Service @Service class OrdersServiceImpl( private val orders: OrderRepository, private val tx: MainDbTransactions, ) : OrdersService { override fun getOrder(orderId: Long): Order { val row = orders.findById(orderId).orElseThrow { ApiException.notFound("order $orderId not found") } val caller = CurrentPrincipal.get() if (caller != null && !caller.hasRole("admin") && row.owner != caller.subject) { throw ApiException.forbidden("not your order") } return row.toApi() } }

The same in Java:

@Service public class OrdersServiceImpl implements OrdersService { private final OrderRepository orders; public OrdersServiceImpl(OrderRepository orders) { this.orders = orders; } @Override public Order getOrder(Long orderId) { var row = orders.findById(orderId).orElseThrow(() -> ApiException.notFound("order " + orderId + " not found")); CurrentPrincipal.get() .filter(caller -> !caller.hasRole("admin") && !row.owner().equals(caller.subject())) .ifPresent(caller -> { throw ApiException.forbidden("not your order"); }); return toApi(row); } }
  • Dependencies: constructor injection of the repositories, transactions and your own beans. Your own packages and @Configuration classes go next to the stubs in app/.
  • Parameters: path, query, header and cookie parameters are method parameters with their types; the body is the last one. Uploaded files are UploadedFile.
  • Errors: throw ApiException (badRequest, notFound, conflict, forbidden, of(status, message)…), or ApiException.withBody(status, message, body) to send the error model of the contract. Other exceptions become 500. See Errors on the wire.
  • Types: date is LocalDate, date-time OffsetDateTime, uuid UUID, duration Duration, decimal BigDecimal; optional properties are nullable; defaults of the contract are applied.

Databases

Interface (<package>.db.<name>)Methods
<Row>RepositoryfindById, existsById, findAll, findAll(offset, limit), count, findBy<Column>, insert, update, deleteById
<Db>QueriesOne method per named query of the contract
<Db>TransactionsinTransaction { … }, readOnly { … }; or annotate a method with @<Db>Transactional

JPA and JDBC have the same interfaces; only the implementation in .internal differs, so you can switch the driver without touching your code. Every database has its own HikariCP data source, transaction manager and Flyway. A constraint violation throws DataConflictException (409 with the constraint name), a missing row RowNotFoundException (404).

Migrations: the first generation writes V1__init.sql, every later change of the schema the next V<n>__*.sql in the dialect of the database, from the difference to schema.snapshot.json. They are never rewritten; dropping data gives a warning. Flyway applies them at startup. Your own migrations go into app/src/main/resources/db/migration/<name>/.

Settings: genexy.databases.<name>.url, username, password, or <NAME>_URL, <NAME>_USERNAME, <NAME>_PASSWORD. The defaults point to the database of the generated compose.yaml.

Parts

A part (kind: part) moves libraries of a service into their own folder, with their own Gradle or Maven build, so a team can keep, say, the payments adapter in its own repository and build it alone. A Spring Boot part has the platform and the database modules, no app/ and no ports.

# services/orders/adapters/payments/.genexy/component.yaml name: payments type: spring-boot kind: part packageName: com.acme.payments modules: - name: payments-db type: postgresql-adapter contract: { $ref: db/payments.yaml }

The service lists it in parts: [{ $ref: adapters/payments }]. With Gradle its settings include the part’s build (includeBuild) and app/ depends on it; with Maven the part is a module of the service’s pom. The application imports the part’s database modules, and application.yml, compose.yaml and the test databases contain its databases. The service reads only the part’s component.yaml, so the two can be generated in any order. Use the same buildTool and its own packageName for the part.

With one port, the application is a plain Spring Boot application. With several, the root context holds your services and the databases, and every port is a child context with its own Tomcat, controllers, error handling, middleware and security. CORS, limits and tokens of one port never affect another.

  • The middleware is the servlet filter PortPolicyFilter: request id, access log, security headers, CORS, body limit and rate limit. Compression is Tomcat’s (server.compression.*).
  • Authentication is the MVC interceptor PortSecurityInterceptor. Every controller method carries an @OperationSecurity annotation generated from the contract.
  • All options: Ports, Middleware & Security.
⚠️

Two settings work differently in Spring Boot. middleware.requestTimeout is not enforced, because synchronous Spring MVC controllers cannot be interrupted, so the generator only warns. validation.unknownProperties: reject applies to the whole component, because Spring Boot has one JSON mapper for all ports. The generator warns when only some ports ask for it. The body limit is checked with Content-Length, so chunked bodies are not limited yet.

Configuration

  1. app/src/main/resources/application.yml (generated: the values of the spec)
  2. app/src/main/resources/config/application.yml (yours)
  3. profiles, environment variables (GENEXY_PORTS_API_PORT), command line (--genexy.ports.api.port=9090)

Read your own settings with @ConfigurationProperties or @Value in app/. Placeholders like ${AUTH_ISSUER:https://sso.example.com/realms/shop} are resolved by Spring.

Tests

TestOwnerChecks
ArchitectureTestsgeneratedapp/ uses only the public types of the modules
PortsTestsgeneratedWith several ports: every operation answers only on its own port
TestDatabasesgeneratedStarts the databases with Testcontainers for the Spring tests
<App>ApplicationTestsyourscontextLoads

Start the application on free ports in your own tests and ask PortRegistry for the addresses:

ConfigurableApplicationContext context = OrdersApplication.run("--genexy.ports.api.port=0"); String base = context.getBean(PortRegistry.class).url("api");

The database tests need Docker for Testcontainers; without Docker they are skipped and the build still passes. With GENEXY_TEST_DATABASES=external they use a running database instead, for example from the generated compose.yaml.

Build and run

Needs a JDK of the chosen Java version. Gradle or Maven comes with the wrapper. In the component folder:

TaskGradleMaven
Build + test./gradlew build./mvnw verify
Run./gradlew :app:bootRun./mvnw -pl app -am spring-boot:run
Jarapp/build/libs/*.jarapp/target/*.jar

On Windows use gradlew.bat and mvnw.cmd. The generator owns every build.gradle.kts and pom.xml: add libraries with dependencies in the component spec, not in the build files.

Not generated yet

  • REST and WebSocket clients, MariaDB as its own dialect, PostGIS, Oracle, MongoDB, Redis, Kafka and NATS modules
  • oneOf and discriminators, response headers and several success statuses, readOnly / writeOnly, streaming
  • /healthz, Actuator as a module, businessErrors
  • apiKey and basic security schemes, Keycloak in compose.yaml
  • A generated Dockerfile (docker.enabled) and Kubernetes manifests; today the Dockerfile is a starter you own