Overview
An API action is an endpoint: an HTTP handler that runs the business logic for a resource. Endpoints live inapps/api/src/resources/<name>/endpoints/, and the filesystem is the route table — there’s no registry to maintain.
| File | Route |
|---|---|
endpoints/list.ts | GET /users |
endpoints/create.ts | POST /users |
endpoints/[userId]/update.ts | PUT /users/{userId} |
endpoints/current.get.ts | GET /users/current |
endpoints/current.patch.ts | PATCH /users/current |
@/endpoint builder. After adding or removing a file, run codegen to refresh the router and contract:
The builder
@/endpoint is the oRPC builder with every middleware in @/middlewares/global already applied. You chain four things onto it:
.output() before it’s sent. There’s no mutable response object to write to.
A real endpoint
This isapps/api/src/resources/users/endpoints/list.ts — admin-only, paginated, with search and a date filter:
db.usersis a generated, typedDbServiceexported from@/db. Its uniform filter API (where,orderBy,ilike,findPage) is the same on every table.deletedAt: nullrespects soft-delete — every table extendsbaseColumns.- The handler
returns the page result directly; oRPC validates it againstlistResultSchema(publicSchema).
Handler arguments
The handler receives a single object. The two you reach for:input— the parsed, typed result of your.input()schema. Already validated; no manual parsing.context— the request context. Gates populate it:isAuthorizedandisAdminguaranteecontext.user;canAccess/canEditload entities into it.
current.get.ts returns the signed-in user straight off the context:
.input() is needed when the endpoint takes no arguments.
Mutations and context
A write readscontext, mutates through the typed service, and returns the fresh row. From current.patch.ts:
Throw, don’t write error responses. To fail a request, throw an
ORPCError — e.g. throw new ORPCError('NOT_FOUND', { message: 'User not found' }). Gates like canAccess and canEdit already do this for you.Custom routes
File-path mounting covers the common cases. To override the method or path explicitly, chain.route(...) — as the Notes plugin does for DELETE /notes/{id}:
See it live
Every endpoint shows up in the Scalar API reference at http://localhost:3001/docs (raw OpenAPI 3.1 at/spec.json), generated from your .input() / .output() schemas — no annotations to write.

Next steps
- Validation — how
.input()/.output()and gates enforce the contract. - API conventions — the rules that keep resources clean as the app grows.
- How Ship works — the resource-owns-everything model behind this.
