Skip to main content

Overview

An API action is an endpoint: an HTTP handler that runs the business logic for a resource. Endpoints live in apps/api/src/resources/<name>/endpoints/, and the filesystem is the route table — there’s no registry to maintain. Every file default-exports an endpoint built on the shared @/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:
The handler returns a value — that return is the response, validated against .output() before it’s sent. There’s no mutable response object to write to.

A real endpoint

This is apps/api/src/resources/users/endpoints/list.ts — admin-only, paginated, with search and a date filter:
What to notice:
  • db.users is a generated, typed DbService exported from @/db. Its uniform filter API (where, orderBy, ilike, findPage) is the same on every table.
  • deletedAt: null respects soft-delete — every table extends baseColumns.
  • The handler returns the page result directly; oRPC validates it against listResultSchema(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: isAuthorized and isAdmin guarantee context.user; canAccess / canEdit load entities into it.
A minimal authorized read — current.get.ts returns the signed-in user straight off the context:
No .input() is needed when the endpoint takes no arguments.

Mutations and context

A write reads context, 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. Scalar API reference

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.