Skip to content

Authoring Routes

A route in Slingshot is the unit of HTTP. Whether it’s auto-generated from an entity or declared in a domain(...), it goes through the same set of concerns: input validation, authorization, response shaping, error handling, observability. This section is the canonical home for each one.

ConcernWhere it’s setPage
Path, method, handlerroute.get/post/...({ path, handler })Routes and Handlers
Request validationrequest: { body, params, query } (Zod)Validation
Response shaperesponses[status]: { schema, transform, dto } + entity dtoDTO Mapping
Auth, permission, rate limit, idempotency, eventsroutes.<op> for entities, route.* for domain routesRoute Policy
Per-route logic before/after the handlermiddleware: [...]Middleware
Error responsesthrown HttpError or respond.notFound() etc.Exception Handling
Authenticated identityactor on the handler contextContext and Actors
Helper utilities for contextgetActor, getRequestTenantId, etc.Context and Actor Helpers
Cursor-based listingpagination config + respond.json({ items, cursor })Pagination
Retry-safe writesidempotency: true on the routeIdempotent Requests
Server-generated request IDautomatic — surfaced via c.get('requestId')Request IDs
Versioned API surfaceversioning: { ... } in app configAPI Versioning
Generated docsautomatic from request + responses schemasOpenAPI

Both flavors share every concern listed above. The difference is what generates them:

Entity routes — auto-generated from defineEntity({ ..., routes: { ... } }). CRUD is free (POST /users, GET /users/:id, etc.). Named operations come from defineOperations({ publishPost: op.transition(...) }) and become additional routes under the entity’s path. Concerns are declared per-op:

defineEntity('Post', {
fields: {
/* ... */
},
routes: {
list: { auth: 'none' }, // public list
create: { permission: { requires: 'post:write' }, dto: 'default' },
operations: {
publish: { permission: { requires: 'post:moderate' } },
},
},
});

Domain routes — declared explicitly inside definePackage({ domains: [...] }) for endpoints that don’t map to entity CRUD. Concerns are declared per-route:

route.post({
path: '/preview',
auth: 'userAuth',
rateLimit: { windowMs: 60_000, max: 30 },
request: { body: PreviewBody },
responses: { 200: { description: 'Rendered', schema: PreviewResult } },
handler: async ({ body, respond }) => respond.json(await render(body.markdown)),
});

For most concerns the surface is identical — auth, permission, rateLimit, idempotency, event, middleware work the same way on either side. Validation is declared on the route in the domain case and auto-derived from defineEntity fields on the entity case (with inputVariants to gate per-route).

The pages in this section are independent — read whichever ones cover the concern you’re working with. New to Slingshot? Read in this order:

  1. Routes and Handlers — the basic shape of a route and what the handler receives.
  2. Validation — Zod schemas at the request boundary.
  3. DTO Mapping — controlling response shape and gating input fields per variant.
  4. Route Policy — auth, permissions, rate limits, idempotency, events on entity routes.
  5. Middleware — per-route or per-package middleware chains.
  6. Exception Handling — typed errors and the framework’s error envelope.

The remaining pages (Pagination, Idempotency, Request IDs, API Versioning, OpenAPI) are focused topics — open them when you need them.

Your routes are tight. The journey continues into the data and security layers:

  1. Working with Data — the entity system in depth, when the route should be auto-generated from a data model.
  2. Security — auth and permissions on top of those routes.
  • Composing an App — return here to revisit how packages, entities, and events fit together at the app level.