/api/ by default: POST /api/{serviceName}/{actionName}. The prefix is configurable via apiPrefix in createServer().
Defining a service
Services vs. entity custom actions
| Entity custom actions | Services | |
|---|---|---|
| Scope | Tied to a single entity instance (:id) | Independent — no parent entity |
| Route | POST /api/tasks/:id/archive | POST /api/notifications/send-email |
| Context | Gets the existing record as 3rd argument | No record — just input and context |
| Use case | Operations on a specific record | Cross-entity workflows, domain services |
Service actions support an optional
path property to override the generated URL segment. When
provided, the path replaces /{serviceName}/{actionName} but still respects the API prefix (e.g.,
path: 'webhooks/stripe' produces POST /api/webhooks/stripe). Prefer the default generated
paths — they keep your routes consistent and discoverable. Use path only when you need a
different URL structure, such as matching an external webhook callback URL.Dependency injection
Services access entities throughinject, just like entity hooks:
Access rules
Services use the same access control as entities — deny-by-default, with the action name as the key:Content descriptors
By default, service actions are JSON-in/JSON-out usings.* schemas. For non-JSON content types (XML, HTML, plain text, binary), use content.* descriptors:
Available descriptors
| Descriptor | Content-Type | TypeScript type |
|---|---|---|
content.xml() | application/xml | string |
content.html() | text/html | string |
content.text() | text/plain | string |
content.binary() | application/octet-stream | Uint8Array |
How it works
Content descriptors implementSchemaLike, so they work in the same body and response properties as s.* schemas. The framework handles everything:
- Response wrapping — the framework sets the correct
Content-Typeheader based on the descriptor - Type inference —
content.xml()makes handler input/output typed asstring,content.binary()asUint8Array - Content-type validation — if a request’s
Content-Typedoesn’t match the descriptor, the framework returns415 Unsupported Media Type - XML MIME flexibility —
content.xml()accepts bothapplication/xmlandtext/xml
Mixing content types
You can mixcontent.* and s.* in the same action — for example, XML input with JSON output:
Optional body
body is optional for actions that don’t receive a request body (GET, DELETE). When omitted, the handler’s input parameter is unknown:
JSON endpoints must always use
s.* schemas for body and response. Content descriptors
are for non-JSON content types only. This ensures all JSON endpoints are fully validated and
typed.Custom response headers and status codes
By default, service actions return200 with application/json. To customize the HTTP response headers or status code, wrap your return value with response():
response() is purely opt-in:
Rules
content-typeis protected and cannot be overridden — the framework always sets it based on the response schema- Custom headers are merged onto the response alongside the framework-managed
content-type - Custom
statusoverrides the default200. Error responses (4xx/5xx from access rules or validation) are not affected response()works with both JSON and content descriptor responses
Entity custom actions also support
response(). The same pattern applies — wrap the return value
to set custom headers or status codes.Request metadata
Service handlers can access raw request metadata viactx.request:
Atomic request handling
When a service handler performs multiple database writes that must succeed or fail together, wrap them indb.transaction():