Your first API
You write a contract in YAML. seampact generates the types, the validators and the server routes. You write the handlers. Words you do not know are in the glossary. At the end, a notes API runs on your machine.
Steps 1 to 3 are the same for every stack. In step 4, open the section of your server: NestJS, Express, Fastify, Hono, AdonisJS, Spring Boot, Laravel, Symfony, FastAPI, Django REST Framework or Django Ninja. The other sections stay closed.
Run every command in the notes-app folder.
1. Install
Section titled “1. Install”On Node.js, follow Install: it creates the notes-app folder, installs the seampact command and runs init. On Spring Boot, Laravel, Symfony and Python, install through your build tool and start from your own app: install in other ecosystems. The packages of your server come in step 4.
2. Write the contract
Section titled “2. Write the contract”The contract describes one collection of records, notes, with four operations. On Node, init wrote contract/api.yaml: replace it with this file. On the other stacks, create it. Keep the first line init writes, the editor modeline (# yaml-language-server: ...): it lets your editor check the contract.
api: name: notes version: 1 kind: product maturity: provisional summary: A small notes api.
pagings: cursor: request: cursor: { query: cursor } limit: { query: limit } response: body: $items links: { next: $next }
schemas: NoteId: { type: string, format: uuid } Note: type: object required: [noteId, title, createdAt] properties: noteId: NoteId title: { type: string, minLength: 1, maxLength: 100 } body: { type: string, maxLength: 2000 } createdAt: { type: string, format: date-time } CreateNote: type: object required: [title] properties: title: { type: string, minLength: 1, maxLength: 100 } body: { type: string, maxLength: 2000 }
surfaces: app: prefix: /v{version} access: { public: "Anyone may call this api: it has no accounts." } routes: notes: kind: collection id: { noteId: NoteId } item: Note ops: list: { paging: { cursor: { maxLimit: 50 } } } get: {} create: { input: CreateNote } delete: {}What each block does:
apinames the api and gives its version.version: 1makes the/v1in every path.kind: productsays this is an API for end users and their clients; it is the default, and the other value isservice, for an internal API with a caller identity.maturity: provisionalsays the API may still change; the values areexperimental,provisionalandstable.pagingsdefines how a list pages. Here a client sendscursorandlimitin the query. The body is the array of records ($items) and the next page goes in aLinkheader ($next).schemasholds the shapes of your data.Noteis what the API returns.CreateNoteis what a client sends to create one.surfaces(a surface is an entry point with a path prefix) holds the routes under a path prefix./v{version}becomes/v1.accesssays who may call them:publicmeans anyone, and the sentence says why.routes.notesis a collection.opspicks the standard operations:list(GET /v1/notes, up to 50 records per page),get(GET /v1/notes/{noteId}),create(POST /v1/notes) anddelete(DELETE /v1/notes/{noteId}).
A contract with one api needs no workspace.yaml: seampact reads api.yaml in the folder. The shape of a contract explains each block further. The contract is plain YAML: every server below reads this same file.
Check it. seampact lint is npx seampact lint on Node; the other stacks run it with the compiler binary: see commands per stack.
seampact lintYou see Linted 1 api(s): 0 error(s), 0 warning(s).
3. Generate the code
Section titled “3. Generate the code”The config tells seampact where the contract is and what to generate. It lives in seampact.config.yaml, at the root of the app. Every stack shares two keys:
sources: notes: path: ./contracttargets: []sources.notes.pathis the folder of the contract. The api inside it is namednotes, which is theapi.nameof the contract.- Each entry of
targets(a target entry) is one kind of output.usenames its generator. A server entry lists the apis it serves (apis: [notes]on Node,api: noteselsewhere) and sets its own options. outDiris the folder of the generated code. Each stack has its own: see Generated files.
Every key is in the config reference. The entry of your server is in step 4. After it, seampact generate writes the code and the OpenAPI document (openapi/notes.yaml). On Node you type npx seampact generate; on the other stacks the build or another command runs it: see commands per stack. Commit the generated files on every stack except Spring Boot: seampact check fails later if they drift from the contract. Never edit them by hand.
4. Write your first handler
Section titled “4. Write your first handler”A handler is the code of one operation: a controller method, a route function or a view, depending on the server. The generated code gives you one interface per resource and calls your handler after it has checked the request. Open the section of your server. Each one has the install lines, the config entry, a minimal handler and the wiring, copied from its guide.
Node: NestJS
Install NestJS and seampact. NestJS decorators need experimentalDecorators in tsconfig.json.
npm install @nestjs/common @nestjs/core @nestjs/platform-express reflect-metadata rxjs zod @seampact/server-runtime @seampact/hooksnpm install --save-dev @seampact/cli @seampact/codegen-ts @seampact/codegen-zod @seampact/codegen-nestjs{ "compilerOptions": { "target": "ES2024", "lib": ["ES2024"], "module": "NodeNext", "moduleResolution": "NodeNext", "types": ["node"], "strict": true, "skipLibCheck": true, "noEmit": true, "experimentalDecorators": true }, "include": ["src"]}Add the targets to seampact.config.yaml, then generate. The files appear in src/__generated__.
outDir: src/__generated__sources: notes: path: ./contracttargets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: validators use: "@seampact/codegen-zod" apis: [notes] - id: server use: "@seampact/codegen-nestjs" apis: [notes] options: bodyValidation: true link: types: types validators: validatorsnpx seampact generateWrite the handlers in src/notes-controller.ts. This one keeps the notes in memory. A real app reads input.query.cursor to page.
import { randomUUID } from "node:crypto";import { Controller } from "@nestjs/common";import * as NotesNest from "./__generated__/server/notes/index.js";import * as Notes from "./__generated__/types/notes/index.js";
const notes = new Map<string, Notes.schemas.Note>();
@Controller()export class NotesController implements NotesNest.NotesEndpoints { @NotesNest.listNotes() listNotes(@NotesNest.listNotes.input() input: Notes.ListNotesInput) { return { status: Notes.Status.Ok, body: { items: [...notes.values()] } }; }
@NotesNest.getNote() getNote(@NotesNest.getNote.input() input: Notes.GetNoteInput) { const note = notes.get(input.path.noteId);
if (note === undefined) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.Ok, body: note }; }
@NotesNest.createNote() createNote(@NotesNest.createNote.input() input: Notes.CreateNoteInput) { const note = { noteId: randomUUID(), createdAt: new Date().toISOString(), ...input.body };
notes.set(note.noteId, note);
return { status: Notes.Status.Created, body: note }; }
@NotesNest.deleteNote() deleteNote(@NotesNest.deleteNote.input() input: Notes.DeleteNoteInput) { if (!notes.delete(input.path.noteId)) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.NoContent }; }}Mount the generated module in src/app-module.ts. A public api needs no hooks, so forRoot() takes none.
import { Module } from "@nestjs/common";import * as NotesNest from "./__generated__/server/notes/index.js";import { NotesController } from "./notes-controller.js";
@Module({ imports: [NotesNest.SeampactModule.forRoot()], controllers: [NotesController],})export class AppModule {}Start the app in src/main.ts. configureApp makes bad JSON and unknown routes answer in the contract’s error format.
import "reflect-metadata";import { NestFactory } from "@nestjs/core";import type { NestExpressApplication } from "@nestjs/platform-express";import * as NotesNest from "./__generated__/server/notes/index.js";import { AppModule } from "./app-module.js";
const app = await NestFactory.create<NestExpressApplication>(AppModule);
NotesNest.configureApp(app);
await app.listen(3000);Install tsx to run TypeScript, check the types and start the app. It listens on port 3000.
npm install --save-dev tsx typescript @types/nodenpx tsc -p .npx tsx src/main.tsNext: the NestJS guide (access hooks, tests, Docker).
Node: Express
Install Express and seampact.
npm install express@5 zod @seampact/server-runtime @seampact/hooksnpm install --save-dev @types/express@5 @seampact/cli @seampact/codegen-ts @seampact/codegen-zod @seampact/codegen-expressAdd the targets to seampact.config.yaml, then generate. The files appear in src/__generated__.
outDir: src/__generated__sources: notes: path: ./contracttargets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: validators use: "@seampact/codegen-zod" apis: [notes] - id: server use: "@seampact/codegen-express" apis: [notes] options: bodyValidation: true link: types: types validators: validatorsnpx seampact generateWrite the handlers in src/notes-handlers.ts. A real app reads input.query.cursor to page.
import { randomUUID } from "node:crypto";import type * as NotesExpress from "./__generated__/server/notes/index.js";import * as Notes from "./__generated__/types/notes/index.js";
const notes = new Map<string, Notes.schemas.Note>();
export const notesHandlers: NotesExpress.NotesEndpoints = { listNotes(input) { return { status: Notes.Status.Ok, body: { items: [...notes.values()] } }; },
getNote(input) { const note = notes.get(input.path.noteId);
if (note === undefined) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.Ok, body: note }; },
createNote(input) { const note = { noteId: randomUUID(), createdAt: new Date().toISOString(), ...input.body };
notes.set(note.noteId, note);
return { status: Notes.Status.Created, body: note }; },
deleteNote(input) { if (!notes.delete(input.path.noteId)) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.NoContent }; },};Wire them in src/main.ts. A public api needs no hooks.
import express from "express";import * as NotesExpress from "./__generated__/server/notes/index.js";import { notesHandlers } from "./notes-handlers.js";
const app = express();
app.use(NotesExpress.router({ handlers: { NotesEndpoints: notesHandlers } }));app.use(NotesExpress.preRoutingErrorHandler);
NotesExpress.checkFrameworkRoutes(app);
app.listen(3000);Install tsx to run TypeScript, then start the app. It listens on port 3000.
npm install --save-dev tsx typescript @types/nodenpx tsx src/main.tsNext: the Express guide (access hooks, tests, Docker).
Node: Fastify
Install Fastify and seampact. Fastify has no decorators: leave experimentalDecorators out of tsconfig.json.
npm install fastify ajv @seampact/server-runtime @seampact/hooksnpm install --save-dev @seampact/cli @seampact/codegen-ts @seampact/codegen-fastifyAdd the targets to seampact.config.yaml, then generate. The files appear in src/__generated__.
outDir: src/__generated__sources: notes: path: ./contracttargets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: server use: "@seampact/codegen-fastify" apis: [notes] options: bodyValidation: true link: types: typesnpx seampact generateWrite the handlers in src/notes-handlers.ts. A real app reads input.query.cursor to page.
import { randomUUID } from "node:crypto";import type * as NotesFastify from "./__generated__/server/notes/index.js";import * as Notes from "./__generated__/types/notes/index.js";
const notes = new Map<string, Notes.schemas.Note>();
export const notesHandlers: NotesFastify.NotesEndpoints = { listNotes() { return { status: Notes.Status.Ok, body: { items: [...notes.values()] } }; },
getNote(input) { const note = notes.get(input.path.noteId);
if (note === undefined) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.Ok, body: note }; },
createNote(input) { const note = { noteId: randomUUID(), createdAt: new Date().toISOString(), ...input.body };
notes.set(note.noteId, note);
return { status: Notes.Status.Created, body: note }; },
deleteNote(input) { if (!notes.delete(input.path.noteId)) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.NoContent }; },};Wire them in src/main.ts. A public api needs no hooks.
import Fastify from "fastify";import * as NotesFastify from "./__generated__/server/notes/index.js";import { notesHandlers } from "./notes-handlers.js";
const app = Fastify();
NotesFastify.collectFrameworkRoutes(app);
await app.register(NotesFastify.plugin, { handlers: { NotesEndpoints: notesHandlers },});
app.setNotFoundHandler((request, reply) => { if (NotesFastify.renderPreRoutingError(request, reply)) return;
reply.code(404).send({ message: "not found" });});
await app.listen({ port: 3000 });Install tsx to run TypeScript, then start the app. It listens on port 3000.
npm install --save-dev tsx typescript @types/nodenpx tsx src/main.tsNext: the Fastify guide (access hooks, tests, Docker).
Node: Hono
Install Hono and seampact.
npm install hono @hono/node-server zod @seampact/server-runtime @seampact/hooksnpm install --save-dev @seampact/cli @seampact/codegen-ts @seampact/codegen-zod @seampact/codegen-honoAdd the targets to seampact.config.yaml, then generate. The files appear in src/__generated__.
outDir: src/__generated__sources: notes: path: ./contracttargets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: validators use: "@seampact/codegen-zod" apis: [notes] - id: server use: "@seampact/codegen-hono" apis: [notes] options: bodyValidation: true link: types: types validators: validatorsnpx seampact generateWrite the handlers in src/notes-handlers.ts. A real app reads input.query.cursor to page.
import { randomUUID } from "node:crypto";import type * as NotesHono from "./__generated__/server/notes/index.js";import * as Notes from "./__generated__/types/notes/index.js";
const notes = new Map<string, Notes.schemas.Note>();
export const notesEndpoints: NotesHono.NotesEndpoints = { listNotes() { return { status: Notes.Status.Ok, body: { items: [...notes.values()] } }; },
getNote(input) { const note = notes.get(input.path.noteId);
if (note === undefined) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.Ok, body: note }; },
createNote(input) { const note = { noteId: randomUUID(), createdAt: new Date().toISOString(), ...input.body };
notes.set(note.noteId, note);
return { status: Notes.Status.Created, body: note }; },
deleteNote(input) { if (!notes.delete(input.path.noteId)) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound };
return { status: Notes.Status.NoContent }; },};Wire them in src/main.ts. A public api needs no hooks.
import { serve } from "@hono/node-server";import { getConnInfo } from "@hono/node-server/conninfo";import { Hono } from "hono";import * as NotesHono from "./__generated__/server/notes/index.js";import { notesEndpoints } from "./notes-handlers.js";
const app = new Hono();
app.onError((error, c) => NotesHono.renderContractError(c, error));
NotesHono.registerRoutes(app, { handlers: { NotesEndpoints: notesEndpoints }, clientAddress: (c) => getConnInfo(c).remote.address,});
NotesHono.checkFrameworkRoutes(app);
serve({ fetch: app.fetch, port: 3000 });Install tsx to run TypeScript, then start the app. It listens on port 3000.
npm install --save-dev tsx typescript @types/nodenpx tsx src/main.tsNext: the Hono guide (access hooks, tests, Docker).
Node: AdonisJS
Create the app with the AdonisJS API starter, and answer the prompts with the defaults.
npm init adonisjs@latest notes-app -- --kit=api --pkg=npmcd notes-appInstall seampact.
npm install @seampact/server-runtime @seampact/hooksnpm install --save-dev @seampact/cli @seampact/codegen-ts @seampact/codegen-adonisThe generated code does not compile with the starter’s lib and unused-variable checks. Replace tsconfig.json.
{ "extends": "@adonisjs/tsconfig/tsconfig.app.json", "compilerOptions": { "rootDir": "./", "jsx": "react", "outDir": "./build", "lib": ["ES2024"], "noUnusedLocals": false, "noUnusedParameters": false }}Create seampact.config.yaml (the contract goes in contract/ of this app), then generate. The files appear in src/__generated__.
outDir: src/__generated__sources: notes: path: ./contracttargets: - id: types use: "@seampact/codegen-ts" apis: [notes] - id: server use: "@seampact/codegen-adonis" apis: [notes] options: bodyValidation: true link: types: typesnpx seampact generateWrite the controller in app/controllers/notes_controller.ts.
import { randomUUID } from 'node:crypto'import type * as NotesAdonis from '../../src/__generated__/server/notes/index.js'import * as Notes from '../../src/__generated__/types/notes/index.js'
const notes = new Map<string, Notes.schemas.Note>()
export default class NotesController implements NotesAdonis.NotesEndpoints { listNotes(input: Notes.ListNotesInput) { return { status: Notes.Status.Ok, body: { items: [...notes.values()] } } }
getNote(input: Notes.GetNoteInput) { const note = notes.get(input.path.noteId)
if (note === undefined) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound }
return { status: Notes.Status.Ok, body: note } }
createNote(input: Notes.CreateNoteInput) { const note = { noteId: randomUUID(), createdAt: new Date().toISOString(), ...input.body }
notes.set(note.noteId, note)
return { status: Notes.Status.Created, body: note } }
deleteNote(input: Notes.DeleteNoteInput) { if (!notes.delete(input.path.noteId)) return { status: Notes.Status.NotFound, code: Notes.ErrorCode.ApiNotFound }
return { status: Notes.Status.NoContent } }}Replace start/routes.ts. A public api needs no hooks.
import router from '@adonisjs/core/services/router'import * as NotesAdonis from '../src/__generated__/server/notes/index.js'
NotesAdonis.registerRoutes(router, { controllers: { NotesEndpoints: () => import('#controllers/notes_controller') },})Edit app/exceptions/handler.ts so bad JSON and unknown routes under /v1 answer in the contract’s error format. Add the import at the top, and the if line in handle.
import * as NotesAdonis from '../../src/__generated__/server/notes/index.js' async handle(error: unknown, ctx: HttpContext) { if (await NotesAdonis.renderPreRoutingError(error, ctx)) return undefined
return super.handle(error, ctx) }Check the types and start it. The app listens on port 3333 by default; the starter’s .env sets it.
npx tsc --noEmitnode ace serveNext: the AdonisJS guide (access hooks, tests, Docker).
Java: Spring Boot
Add the plugin and the dependencies to build.gradle.kts. The plugin is on the Gradle Plugin Portal and on Maven Central; until the Portal approves it, add Maven Central to the plugin repositories in settings.gradle.kts.
pluginManagement { repositories { gradlePluginPortal() mavenCentral() }}plugins { java id("org.springframework.boot") version "4.1.1" id("io.github.jubasse.seampact")}
dependencies { implementation("org.springframework.boot:spring-boot-starter-webmvc") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("io.github.jubasse:seampact-hooks")}Add the entry to seampact.config.yaml. Generated code stays out of Git: the build writes it.
outDir: build/generated/sources/seampact/mainsources: notes: path: ./contracttargets: - id: notes use: io.github.jubasse:seampact-spring-boot api: notes options: { package: com.example.notes.api }Maven? Add the runtime and the plugin to pom.xml instead, at the same version (set <seampact.version> once in <properties>), and set outDir to target/generated-sources/seampact. The Spring Boot guide has the details.
<dependency> <groupId>io.github.jubasse</groupId> <artifactId>seampact-spring-runtime</artifactId> <version>${seampact.version}</version></dependency><plugin> <groupId>io.github.jubasse</groupId> <artifactId>seampact-maven-plugin</artifactId> <version>${seampact.version}</version> <executions> <execution> <goals> <goal>generate</goal> <goal>check</goal> </goals> </execution> </executions></plugin>./gradlew build (or ./mvnw verify) generates the code, then compiles it with yours. Write a controller. notes is your own store.
@RestControllerclass NotesController implements NotesEndpoints { @Override @GetNote public GetNoteResult getNote(GetNoteInput input) { Note note = notes.get(input.path().noteId());
return note == null ? new GetNoteErrors.ApiNotFound() : new GetNoteResult.Status200(note); }}Spring finds the generated configuration class by component scan when package sits under your application’s package. Otherwise import it.
@SpringBootApplication@Import(NotesSeampactConfiguration.class)class NotesApplication {}Start it. The app listens on port 8080.
./gradlew bootRunNext: the Spring Boot guide (hooks, tests, Docker).
PHP: Laravel
Install seampact from Packagist.
composer require seampact/laravelAdd the entry to seampact.config.yaml, then generate. The classes go into App\Generated\Notes.
outDir: app/Generatedsources: notes: path: ./contracttargets: - id: notes out: Notes use: seampact/laravel api: notes options: { rules: seampact-consistent }vendor/bin/seampact generate# orphp artisan seampact:generateWrite a controller that implements the generated interface, and bind the interface in a service provider.
final class NotesController implements NotesEndpoints{ public function getNote(GetNoteRequest $request): GetNoteResult { $note = $this->notes->get($request->contractInput()->path->noteId);
return $note === null ? GetNoteErrors::apiNotFound() : new GetNoteResponse200($note); }}$this->app->bind(NotesEndpoints::class, NotesController::class);List the generated provider, and call the route macro once.
return [ App\Generated\Notes\Laravel\NotesSeampactServiceProvider::class,];
// bootstrap/app.php, with use Illuminate\Support\Facades\Route;->withRouting( web: __DIR__.'/../routes/web.php', then: fn () => Route::seampact('notes'),)Start it. The app listens on port 8000.
php artisan serveNext: the Laravel guide (hooks, tests, Eloquent).
PHP: Symfony
Install seampact from Packagist.
composer require seampact/symfonyAdd the entry to seampact.config.yaml. Map outDir in composer.json ("App\\": "src/" covers src/Generated). Then generate.
outDir: src/Generatedsources: contract: { path: contract }targets: - id: notes out: Notes use: seampact/symfony api: notesvendor/bin/seampact generateImplement the generated interface as a service, and mark each method with #[SeampactOperation].
use App\Generated\Notes\GetNoteErrors;use App\Generated\Notes\GetNoteInput;use App\Generated\Notes\GetNoteResponse200;use App\Generated\Notes\GetNoteResult;use App\Generated\Notes\Symfony\SeampactOperation;use App\Generated\Notes\NotesEndpoints;
final class NotesController implements NotesEndpoints{ #[SeampactOperation('getNote')] public function getNote(GetNoteInput $input): GetNoteResult { $note = $this->notes->find($input->path->noteId);
return $note === null ? GetNoteErrors::apiNotFound() : new GetNoteResponse200($note); }}List the api’s bundle and import its routes.
App\Generated\Notes\Symfony\NotesSeampactBundle::class => ['all' => true],seampact_notes: resource: notes type: seampactStart it. The app listens on port 8000.
php -S 127.0.0.1:8000 -t publicNext: the Symfony guide (hooks, tests, Doctrine).
Python: FastAPI
Install seampact from PyPI, with the compiler it needs to read a contract folder.
python3 -m venv .venv && . .venv/bin/activatepip install "seampact-fastapi[generate]"pip install "seampact-ir[compiler]"Add the entry to seampact.config.yaml, then generate. The code goes into generated/notes/.
outDir: generatedopenapi: nonesources: notes: path: ./contracttargets: - id: notes out: notes use: pypi:seampact-fastapi api: notes options: { rules: seampact-consistent }seampact generateWrite a handler class and the factory that builds it. NoteRepository and get_notes are your own code.
from fastapi import Dependsfrom generated.notes.operations import GetNoteErrors, GetNoteRequest, GetNoteResponse200, GetNoteResult
class NotesEndpoints: def __init__(self, notes: NoteRepository) -> None: self.notes = notes
async def get_note(self, request: GetNoteRequest) -> GetNoteResult: note = await self.notes.get(request.input.path.note_id)
if note is None: return GetNoteErrors.api_not_found()
return GetNoteResponse200(note)
def notes_endpoints(notes: NoteRepository = Depends(get_notes)) -> NotesEndpoints: return NotesEndpoints(notes)Bind the contract in your app. A public api needs no hooks. Make bind_notes() the last call, after your own middleware.
from fastapi import FastAPIfrom generated.notes.fastapi import NotesHandlerFactories, bind_notes
app = FastAPI()
bind_notes( app, handlers=NotesHandlerFactories(notes=notes_endpoints),)Start it. The app listens on port 8000.
uvicorn main:appNext: the FastAPI guide (hooks, tests, Deploy).
Python: Django REST Framework
Install seampact from PyPI, with the compiler it needs to read a contract folder.
python3 -m venv .venv && . .venv/bin/activatepip install "seampact-drf[generate]"pip install "seampact-ir[compiler]"Add the entry to seampact.config.yaml, then generate. The code goes into generated/notes/.
outDir: generatedopenapi: nonesources: notes: path: ./contracttargets: - id: notes out: notes use: pypi:seampact-drf api: notes options: { rules: seampact-consistent }seampact generate# orpython manage.py seampact_generateWrite a handler class and the factory that builds it.
from django.http import HttpRequestfrom generated.notes.operations import GetNoteErrors, GetNoteRequest, GetNoteResponse200, GetNoteResult
class NotesHandlers: def __init__(self, request: HttpRequest) -> None: self.request = request
def get_note(self, request: GetNoteRequest) -> GetNoteResult: note = Note.objects.filter(pk=request.input.path.note_id).first()
if note is None: return GetNoteErrors.api_not_found()
return GetNoteResponse200(body=to_contract(note))
def notes_handlers(request: HttpRequest) -> NotesHandlers: return NotesHandlers(request)Two files of your Django project. A public api needs no hooks.
INSTALLED_APPS = [ "django.contrib.contenttypes", "django.contrib.auth", "rest_framework", "seampact.django",]
MIDDLEWARE = [ "generated.notes.django.NotesBoundary", "django.middleware.common.CommonMiddleware",]
SEAMPACT = { "notes": { "handlers": {"notes": "notes.api.notes_handlers"}, },}urlpatterns = [ path("", include("generated.notes.urls")), path("admin/", admin.site.urls),]Start it. The app listens on port 8000.
python manage.py runserverNext: the Django REST Framework guide (hooks, tests, Deploy). For production, serve with gunicorn or uvicorn.
Python: Django Ninja
Install seampact from PyPI, with the compiler it needs to read a contract folder.
python3 -m venv .venv && . .venv/bin/activatepip install "seampact-ninja[generate]"pip install "seampact-ir[compiler]"Add the entry to seampact.config.yaml, then generate. The code goes into generated/notes/.
outDir: generatedopenapi: nonesources: notes: path: ./contracttargets: - id: notes out: notes use: pypi:seampact-ninja api: notes options: { views: sync }seampact generate# orpython manage.py seampact_generateWrite a handler class and the factory that builds it.
from django.http import HttpRequestfrom generated.notes.operations import GetNoteErrors, GetNoteRequest, GetNoteResponse200, GetNoteResult
class NotesHandlers: def __init__(self, request: HttpRequest) -> None: self.request = request
def get_note(self, request: GetNoteRequest) -> GetNoteResult: note = Note.objects.filter(pk=request.input.path.note_id).first()
if note is None: return GetNoteErrors.api_not_found()
return GetNoteResponse200(to_contract(note))
def notes_handlers(request: HttpRequest) -> NotesHandlers: return NotesHandlers(request)Three files of your Django project. A public api needs no hooks.
INSTALLED_APPS = [ "django.contrib.contenttypes", "django.contrib.auth", "ninja", "seampact.django",]
MIDDLEWARE = [ "generated.notes.django.NotesBoundary", "django.middleware.common.CommonMiddleware",]from generated.notes.ninja import NotesHandlerFactories, bind_notesfrom ninja import NinjaAPI
api = NinjaAPI(urls_namespace="notes")bind_notes( api, prefix="", handlers=NotesHandlerFactories(notes=notes_handlers),)urlpatterns = [ path("", api.urls), path("admin/", admin.site.urls),]Start it. The app listens on port 8000.
python manage.py runserverNext: the Django Ninja guide (hooks, tests, Deploy). For production, serve with gunicorn or uvicorn.
A public api needs no hooks. Hooks are the functions that check who calls; the guide of your server shows them.
5. Run it
Section titled “5. Run it”Start the app with the command in your section. In another terminal, create a note. Replace 3000 with your port: 3333 on AdonisJS, 8080 on Spring Boot, 8000 on Laravel, Symfony and Python.
curl -i -X POST localhost:3000/v1/notes -H 'content-type: application/json' -d '{"title":"Buy milk"}'You see 201 and the note with its noteId. curl localhost:3000/v1/notes lists it.
Send an empty title.
curl -i -X POST localhost:3000/v1/notes -H 'content-type: application/json' -d '{"title":""}'You see 400 and an application/problem+json body whose issues names /title. The generated validators refused it before your handler ran.
Test a route from code: every guide has a “Test your routes” section (NestJS, Express, Fastify, Hono, AdonisJS, Spring Boot, Laravel, Symfony, FastAPI, Django REST Framework, Django Ninja).
Something fails? See troubleshooting.
Next: call it from React. More on your server: its guide, linked at the end of your section.