/api/ by default: POST /api/{serviceName}/{actionName}. The prefix is configurable via apiPrefix in createServer().
Defining a service
Services vs. entity custom actions
Use entity custom actions when the operation targets a specific record (archive a task, mark as complete). Use services when the operation spans multiple entities or doesn’t belong to any single entity.
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
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():