Reading about Carrier is one thing. Building with it is another. This interlude takes thirty minutes from a blank directory to a running service with a generated API contract, schema migrations, and a manifest you can govern.
The example is small but realistic — a tenant-scoped patient-intake service with one model, named types, an action, a route, and a policy. Adapt the field names and the domain language to your own use case as you go. If you are working with an AI agent, hand it this chapter and the construct selection guide in Appendix A; you will be reviewing source within minutes, not writing it.
0 Before You Begin (1 min)
You need a Carrier toolchain on your PATH. From the reference repository, build it once with Cargo:
cargo run -p carrierc -- --help
Or invoke carrier directly if it is already installed in your environment. Confirm it works before proceeding.
1 Scaffold the Project (2 min)
mkdir patient-intake
cd patient-intake
mkdir -p src/00_models src/10_types src/20_actions src/30_routes
Carrier follows the import graph rooted at src/main.carrier. We will create that file next, alongside one file per layer. The numeric prefixes are not cosmetic — they keep files sorted in dependency order: models before types, types before actions, actions before routes.
2 Declare the Service (3 min)
Open src/main.carrier and declare the service, the JWT auth scheme, and the import graph:
service PatientIntake {openapi {title: "Patient Intake API"version: "0.1.0"} server {host: env("HOST", "0.0.0.0")port: env_int("PORT", 4500)}} auth jwt StaffAuth {issuer: env("JWT_ISSUER", "carrier")audience: env("JWT_AUDIENCE", "carrier-users")secret: env("JWT_SECRET", "local-dev-secret")roles_claim: "roles"} import "./00_models/intake_submission"import "./10_types/intake_contracts"import "./20_actions/submit_intake"import "./30_routes/intake_routes"3 Add a Model with Policy (5 min)
Open src/00_models/intake_submission.carrier. Notice how the model, the tenant scope, the role rules, and the PII annotation all live in the same place — and a reviewer can see in one glance that this is regulated, tenant-scoped, role-protected data:
enum IntakeStatus {
pending
in_review
completed
} model IntakeSubmission {id: UUIDorg_id: String @indexpatient_email: String @email @pii(category: "contact")status: IntakeStatusnotes: String?version: Int @versioncreated_at: Timeupdated_at: Time} policy IntakeSubmission {tenant_field: org_idread: roles [front_desk, clinician]write: roles [front_desk]}4 Add Named Types (3 min)
Open src/10_types/intake_contracts.carrier. Named types give the API a shared vocabulary that survives schema evolution:
type SubmitIntakeRequest {patient_email: String @emailnotes: String?} type SubmitIntakeResponse {id: UUIDstatus: IntakeStatus}5 Add the Action (4 min)
Open src/20_actions/submit_intake.carrier. The action owns the business operation: identity capture, the transaction, the structured log, and the typed return. The route, in the next step, will stay thin:
action submit_intake(payload: SubmitIntakeRequest) -> SubmitIntakeResponse {let actor = auth.current_user() transaction {let submission = IntakeSubmission.create({patient_email: payload.patient_emailnotes: payload.notesstatus: IntakeStatus.pending})logs.info("intake submitted", {action_name: "submit_intake"submission_id: submission.idactor_id: actor.idactor_email: actor.email})return {id: submission.idstatus: submission.status}}}6 Add the Route (2 min)
Open src/30_routes/intake_routes.carrier. The route is the HTTP edge — protected, role-restricted, typed in and out, and delegating immediately to the action:
route POST "/intake/submit" protect StaffAuth roles [front_desk] -> SubmitIntakeResponse {input: SubmitIntakeRequest handler {return submit_intake(input)}}7 Verify and Generate (5 min)
Run the standard development loop:
carrier fmtcarrier checkcarrier build --target nodeThen produce the governance artifacts:carrier openapi > openapi.jsoncarrier migrate generate8 Take Stock (5 min)
Look at what you have:
- openapi.json — the public contract for POST /intake/submit, ready for the integration team
- Generated SQL migrations — ready for DBA review before deployment
- .carrier/manifest.json — machine-readable evidence of routes, models, types, and policies
- A compiled Node service — ready to run against a Postgres sandbox
In thirty minutes, you have produced a service that obeys tenant scoping, role-based access, contract-first API design, and schema-aware migrations — without writing a controller, a validator, a serializer, or an OpenAPI annotation. Every file you touched is small, declarative, and reviewable.
Domain in. Service out. Same shape every time.
Part Two
Core Language Model
The constructs that carry architectural meaning
❦