Endpoints mount by file path
Each file underapps/api/src/resources/<name>/endpoints/ is one endpoint. Its method and HTTP path come from the file name and folder structure:
| File | Method | Path |
|---|---|---|
users/endpoints/list.ts | GET | /users |
users/endpoints/create.ts | POST | /users |
users/endpoints/[userId]/update.ts | PUT | /users/{userId} |
users/endpoints/[userId]/delete.ts | DELETE | /users/{userId} |
users/endpoints/current.get.ts | GET | /users/current |
users/endpoints/dev-verify-email.post.ts | POST | /users/dev-verify-email |
scripts/codegen-router.ts:
- CRUD names —
list→GET(collection),create→POST(collection),get→GET,update→PUT,delete→DELETE. - Method suffix —
name.post.ts→POST /name,status.get.ts→GET /status. Use this for non-CRUD actions. [param]folders —[userId]/becomes the{userId}dynamic segment.- Resource name = path prefix —
resources/users/→/users/*.
.route(...), which overrides the convention:
Contract-first, generated
oRPC splits a thin contract (routes + schemas) from the router (implementations). Ship generates both from the filesystem, so the wiring is always real:codegen-router.ts discovers every resource and endpoint, then writes two files. Both are generated — never edit them by hand.
src/contract.ts carries one route per endpoint:
contract.ts
src/router.ts implements that contract by slotting each endpoint into place, and exports the AppClient type the web app imports:
router.ts
handlers/* (so mutation-event side effects register) and emits a parallel spec-only router that feeds the Scalar docs. Run it after adding or removing an endpoint file — pnpm --filter api dev runs it in --watch mode for you.
Request flow
server.ts builds an ORPCContext for each request — cookies, headers, and the better-auth session resolved into context.user. The oRPC handler then runs the endpoint’s global middlewares, its gates, and .input() validation before the handler.
Session context
Auth is contextual, not a route flag.serverConfig.resolveUser reads the better-auth session from the request headers and attaches the user row to context.user when present:
server-config.ts
.use(isAuthorized) to require a signed-in context.user; add .use(isAdmin) to require an admin. See Middlewares.
See also
- Middlewares — global and per-endpoint gate composition
- API overview — the full Hono + oRPC story
- How Ship works — the resource-owns-everything model
