Add seampact to an existing app
You keep your routes. seampact takes over the ones you put in the contract, and the rest stay yours.
You need a working app on one of the supported servers, kept in a git repo to see what changes. The server-specific steps are in the section of your server (the section “Add it to your server”); the rest is the same for every stack.
The examples use a shop app with GET /health, POST /auth/login and a products controller.
Describe the routes you move
Section titled “Describe the routes you move”Start with the products routes. Copy the schemas from your DTOs. Create contract/api.yaml.
api: name: shop version: 1 kind: product maturity: provisional summary: The shop api.
schemas: ProductId: { type: string, format: uuid } Product: type: object required: [id, name, priceCents] properties: id: ProductId name: { type: string, minLength: 1, maxLength: 100 } priceCents: { type: integer, minimum: 0 } CreateProduct: type: object required: [name, priceCents] properties: name: { type: string, minLength: 1, maxLength: 100 } priceCents: { type: integer, minimum: 0 }
surfaces: app: prefix: "" pathVersion: none access: { public: "Anyone may call the catalogue." } routes: products: kind: collection singular: product id: { id: ProductId } item: Product ops: list: { paging: none } get: {} create: { input: CreateProduct }pathVersion: noneandprefix: ""keep your paths:/products, not/v1/products.singular: productnames the operationsgetProductandcreateProduct. Without it, an id param namedidgivesgetId.paging: nonekeeps the list a bare array, as your clients expect.
seampact lintYou see Linted 1 api(s): 0 error(s).
Add it to your server
Section titled “Add it to your server”Open the section of your server. NestJS shows the full path with the shop app; the other servers list what differs and link their guide.
Node: NestJS
Install the packages, in the app folder.
npm install @seampact/server-runtime @seampact/hooksnpm install --save-dev @seampact/cli @seampact/codegen-ts @seampact/codegen-zod @seampact/codegen-nestjsThe app needs NestJS 12 and experimentalDecorators. Your own DTOs need emitDecoratorMetadata too, for the class-validator ValidationPipe. Run the app from compiled output: tsx does not emit decorator metadata, so validation of your DTOs would silently do nothing.
Create seampact.config.yaml. This one keeps class-validator; @seampact/codegen-zod and @seampact/codegen-yup work too.
outDir: src/__generated__sources: shop: path: ./contracttargets: - id: types use: "@seampact/codegen-ts" apis: [shop] - id: validators use: "@seampact/codegen-class-validator" apis: [shop] - id: server use: "@seampact/codegen-nestjs" apis: [shop] options: bodyValidation: true link: types: types validators: validatorsnpx seampact generateYou see one line per target, and the files in src/__generated__ and openapi/shop.yaml. Commit them.
Move one controller
Delete the DTO classes: the generated class-validator classes replace them. Implement the generated interface and swap the route decorators.
import { Controller } from "@nestjs/common";import * as ShopNest from "../__generated__/server/shop/index.js";import * as Shop from "../__generated__/types/shop/index.js";
@Controller()export class ProductsController implements ShopNest.ProductsEndpoints { @ShopNest.getProduct() getProduct(@ShopNest.getProduct.input() input: Shop.GetProductInput) { const product = products.get(input.path.id);
if (product === undefined) return { status: Shop.Status.NotFound, code: Shop.ErrorCode.ApiNotFound };
return { status: Shop.Status.Ok, body: product }; }}A list handler returns the array: { status: Shop.Status.Ok, body: [...products.values()] }.
Controllers for routes outside the contract stay as they are.
npx tsc -p .You see no output.
Wire the module and the app
In AppModule, import the generated module.
imports: [ShopNest.SeampactModule.forRoot()],In main.ts, add this line before app.listen. It sets up the JSON parser and the error filter of the contract routes only: your other routes keep their behaviour.
ShopNest.configureApp(app);Start the app. You see it refuse to start: 2 framework routes are outside the contract. The boot check lists your other routes, here /auth/login and the Swagger routes. Allow them as in “Keep your other routes” below.
Node: Express
Follow Set up of the Express guide to install it, then add to seampact.config.yaml: the three Node targets (@seampact/codegen-ts, @seampact/codegen-zod, @seampact/codegen-express) with apis: [shop].
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Mount the router in your existing app, without a path, before the routes you keep (Wire it in).
The app refuses to start while a route outside the contract is not allowed: checkFrameworkRoutes throws and names it. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Node: Fastify
Follow Set up of the Fastify guide to install it, then add to seampact.config.yaml: @seampact/codegen-ts and @seampact/codegen-fastify with apis: [shop].
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Register the plugin without a prefix (Wire it in).
The plugin refuses to start while a route outside the contract is not allowed. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Node: Hono
Follow Set up of the Hono guide to install it, then add to seampact.config.yaml: @seampact/codegen-ts, @seampact/codegen-zod and @seampact/codegen-hono with apis: [shop].
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Call registerRoutes on your existing app (Wire it in).
checkFrameworkRoutes throws at start while a route outside the contract is not allowed. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Node: AdonisJS
Follow Set up of the AdonisJS guide to install it, then add to seampact.config.yaml: @seampact/codegen-ts and @seampact/codegen-adonis with apis: [shop].
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Call registerRoutes in start/routes.ts outside any prefixed group (Wire it in).
The app refuses to start while a route outside the contract is not allowed. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Java: Spring Boot
Follow Set up of the Spring Boot guide to install it, then add to seampact.config.yaml: the plugin to your build and one entry with use: io.github.jubasse:seampact-spring-boot and api: shop.
Build to generate (the build writes the files, so you do not commit them). Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Keep your @RestController: implement the generated interface and the mapping annotations replace yours; Spring finds the generated configuration class by component scan (Wire it in).
The Spring context fails to start, and the build log lists every problem. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
PHP: Laravel
Follow Set up of the Laravel guide to install it, then add to seampact.config.yaml: composer require seampact/laravel and one entry with use: seampact/laravel and api: shop.
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. List the generated provider and call Route::seampact('shop') once (Wire it in).
The app refuses to boot, and the message lists every problem. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
PHP: Symfony
Follow Set up of the Symfony guide to install it, then add to seampact.config.yaml: composer require seampact/symfony and one entry with use: seampact/symfony and api: shop.
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. List the api’s bundle and import its routes (Wire it in).
The app refuses to boot, and the message lists every problem. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Python: FastAPI
Follow Set up of the FastAPI guide to install it, then add to seampact.config.yaml: use: pypi:seampact-fastapi, api: shop.
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Call bind_shop() last in your existing app (Wire it in).
The app refuses to start: uvicorn exits with Application startup failed. Exiting.. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Python: Django REST Framework
Follow Set up of the Django REST Framework guide to install it, then add to seampact.config.yaml: use: pypi:seampact-drf, api: shop.
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Add the boundary to MIDDLEWARE, the settings and the urls (Wire it in).
The app refuses to start: gunicorn stops with Worker failed to boot., and a Django system check reports it. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Python: Django Ninja
Follow Set up of the Django Ninja guide to install it, then add to seampact.config.yaml: use: pypi:seampact-ninja, api: shop.
Generate, then commit the files. Implement the generated interface in your existing controller for the products routes, and delete the DTO or request classes it replaces: Write a handler. Call bind_shop() on your NinjaAPI (Wire it in).
The app refuses to start: gunicorn stops with Worker failed to boot., and a Django system check reports it. The boot check lists your other routes: allow-list them as in “Keep your other routes” below.
Keep your other routes
Section titled “Keep your other routes”Routes of yours that are not in the contract stay as they are. The boot check lists them and refuses to start until each one is allowed. List them under frameworkRoutes of the server entry, then generate again. A NestJS example:
artefacts: nativeDocs: true frameworkRoutes: - { path: "/health", reason: "Health check, not in the contract yet." } - { path: "/auth/login", reason: "Login, not part of the shop contract." } - { path: "/docs", reason: "Swagger UI." } - { path: "/docs/**", reason: "Swagger UI files." } - { path: "/docs-json", reason: "Swagger document." } - { path: "/docs-yaml", reason: "Swagger document." }The shop api serves its routes at the root, so the boot check reads every route, the docs routes included. nativeDocs and the lines for the docs are in OpenAPI and docs. You see your old routes working.
Check it
Section titled “Check it”seampact checkYou see check: the generated files and documents match the committed ones. Run git diff --stat to see what the adoption touched. On Spring Boot, the build runs check; the other stacks type the command from commands per stack.
Move the next route
Section titled “Move the next route”Add the route to the contract, under routes: of the app surface.
health: { method: get, path: health, output: Health }Add a Health schema with a status string. Move the handler to the generated interface. Delete the /health line from frameworkRoutes, then generate again.
You see GET /health served by the generated route.
What changes for your clients
Section titled “What changes for your clients”On the routes you move:
- Errors become
application/problem+json, not your framework’s default error body (for example Nest’s{ message, error, statusCode }). - An id typed
uuidthat is malformed answers 400, not 404. - Query keys the contract does not declare are refused with 400.
- A list gains the optional
cursorandlimitquery params.
Routes you did not move keep their own behaviour.
Something fails? See troubleshooting. Every config key: the config reference.
Next: project layout, or check it in CI.