Response Format
New Vitae.ai endpoints should use explicit JSON:API serializers at the controller boundary. The goal is that every API response is predictable, regardless of whether the frontend, CLI, MCP server, or a customer integration consumes it.
Serializers must live in packages/serializers, not inline in controllers or API modules. Controllers should call serializeSingle(request, Serializer, doc) or serializeCollection(request, Serializer, paginatedResult).
Single resource
{
"data": {
"id": "client_01h...",
"type": "client",
"attributes": {
"name": "Acme Corp",
"createdAt": "2026-05-15T13:00:00.000Z"
}
},
"links": {
"self": "/v1/clients/client_01h..."
}
}Resource list
{
"data": [
{
"id": "job_01h...",
"type": "job",
"attributes": {
"title": "Senior Backend Engineer"
}
}
],
"links": {
"self": "/v1/jobs?page=1&limit=25",
"pagination": {
"page": 1,
"limit": 25,
"total": 125,
"totalPages": 5,
"hasNextPage": true,
"hasPreviousPage": false
}
}
}Action result
Workflow commands still return a resource envelope. The changed resource is returned in data; secondary entities can be included in meta.
{
"data": {
"id": "inbox_item_01h...",
"type": "inbox_item",
"attributes": {
"status": "accepted"
}
}
}Errors
{
"error": {
"code": "validation_error",
"message": "Validation failed",
"details": [
{ "field": "email", "message": "must be a valid email" }
],
"requestId": "req_01h..."
}
}Serializer rules
- Every resource exposes
idas a string andtypeas a stable resource type. - Public fields live under
attributes. - Relationships live under
relationshipswhen serialized. - Internal database fields like
_id,__v, raw ObjectIds, and deleted flags are not returned unless explicitly part of the public contract. - Dates are ISO 8601 strings.
- Field names are
camelCase. - Lists are always arrays under
data. - Pagination lives under
links.pagination. - Errors always use the
errorenvelope.
Migration note
The API has a global compatibility serializer that normalizes normal controller returns into { data, meta } while endpoints are migrated. JSON:API documents returned from explicit serializers bypass that compatibility wrapper.
Endpoints that manually write to the Express response object, stream files, or intentionally opt out with @SkipResponseSerialization() are compatibility surfaces and must document their custom response format.
Current manual-response surfaces:
- Stripe webhooks
- CSV, XLSX, attachment, and QR-code downloads
- OAuth redirects and revocation callbacks
- short URL redirects
- streaming agent chat
- candidate search streaming/export responses