Writing Resources
Resora is designed to be extended.
You rarely use Resource directly in real applications. Instead, you create custom resource classes that define how your domain objects are transformed before being returned to the client.
This section explains how to create:
- Custom
Resourceclasses - Custom
ResourceCollectionclasses - Paginated and cursor-aware collections
Extending Resource
A Resource represents a single entity transformation.
To create one, extend the base Resource class and override the data() method.
Basic Resource Extension
import { Resource } from 'resora';
class UserResource extends Resource {
data() {
return this.toArray();
}
}Usage:
const resource = { id: 1, name: 'John Doe' };
const userResource = new UserResource(resource);
userResource.getBody();Output:
{
"data": {
"id": 1,
"name": "John Doe"
}
}Transforming Fields
You can shape the output however you like inside data().
class UserResource extends Resource {
data() {
return {
id: this.id,
name: this.name,
custom: 'data',
};
}
}Usage:
const resource = { id: 1, name: 'John Doe' };
const userResource = new UserResource(resource);
userResource.getBody();Output:
{
"data": {
"id": 1,
"name": "John Doe",
"custom": "data"
}
}Key point:
this.idandthis.nameare accessible because the base class proxies properties from the original resource.
Conditional Attributes
Use conditional helpers to keep data() declarative without verbose if blocks.
For complete usage patterns and examples, see Conditional Rendering.
this.when(condition, value | () => value)this.whenNotNull(value)this.mergeWhen(condition, object | () => object)
class UserResource extends Resource {
data() {
return {
id: this.id,
email: this.whenNotNull(this.email),
role: this.when(this.isAdmin, 'admin'),
...this.mergeWhen(this.isAdmin, {
permissions: ['manage-users'],
}),
};
}
}If a condition fails, the attribute is omitted from the final serialized payload.
Metadata APIs: with() vs withMeta()
Resora supports two metadata patterns:
with()as a class hook (override in custom classes)withMeta()as a typed fluent API (chain in handlers/services)
Class hook: with()
Use this when the resource class should always contribute metadata.
class UserResource extends Resource {
with() {
return {
source: 'user-resource',
apiVersion: 'v1',
};
}
}When json() runs, this metadata is merged into meta automatically.
Fluent API: withMeta()
Use this for per-request metadata and strong TypeScript inference.
const body = new UserResource({ id: 1, name: 'John' })
.withMeta((resource) => ({ actor: resource.name }))
.withMeta({ traceId: 'abc-123' })
.getBody();Merge behavior
Metadata is merged (deeply) in this order:
- Built-in defaults (e.g.
pagination/cursorfor collections) - Class hook metadata from
with() - Fluent metadata from
withMeta(...)and/orwith({...})
So custom metadata does not replace important defaults unless you explicitly overwrite the same key.
Creating Collections From a Resource
Every Resource subclass can generate a collection using the static collection() method.
const resource = [{ id: 1, name: 'John Doe' }];
const collection = userResource.collection(resource);
collection.getBody();Output:
{
"data": [
{
"id": 1,
"name": "John Doe",
"custom": "data"
}
]
}The returned instance is a ResourceCollection.
Extending ResourceCollection
When you need more control over collections, extend ResourceCollection directly.
You must define:
collects→ the Resource class used per itemdata()→ how the transformed array is returned
Non-Paginated Collection
import { ResourceCollection } from 'resora';
class UserCollection<R extends User[]> extends ResourceCollection<R> {
collects = UserResource;
data() {
return this.toArray();
}
}Usage:
const resource = [{ id: 1, name: 'John Doe' }];
const collection = new UserCollection(resource);
collection.getBody();Output:
{
"data": [
{
"id": 1,
"name": "John Doe",
"custom": "data"
}
]
}Paginated Collections
If your collection input contains pagination metadata:
const resource = {
data: [{ id: 1, name: 'John Doe' }],
pagination: { currentPage: 1, total: 10 },
};Using the same UserCollection:
const collection = new UserCollection(resource);
collection.getBody();Output:
{
"data": [
{
"id": 1,
"name": "John Doe",
"custom": "data"
}
],
"meta": {
"currentPage": 1,
"total": 10
}
}Pagination metadata is automatically extracted into meta.pagination.
Cursor-Based Collections
If your input includes cursor metadata:
const resource = {
data: [{ id: 1, name: 'Acm. Inc.' }],
cursor: { previous: 'abc', next: 'def' },
};Output:
{
"data": [
{
"id": 1,
"name": "Acm. Inc.",
"custom": "data"
}
],
"cursor": {
"previous": "abc",
"next": "def"
}
}Cursor metadata is automatically mapped to cursor.
Pagination + Cursor Together
If both are present:
const resource = {
data: [{ id: 1, name: 'Acm. Inc.' }],
pagination: { currentPage: 1, total: 10 },
cursor: { previous: 'abc', next: 'def' },
};Output:
{
"data": [
{
"id": 1,
"name": "Acm. Inc.",
"custom": "data"
}
],
"meta": {
"currentPage": 1,
"total": 10
},
"cursor": {
"previous": "abc",
"next": "def"
}
}Both metadata types are preserved.
Chaining With Extended Resources
Both Resource and ResourceCollection support chaining.
Example:
collection.additional({ status: 'success' }).getBody();Output:
{
"data": [
{
"id": 1,
"name": "Acm. Inc.",
"custom": "data"
}
],
"status": "success"
}Outgoing Response Hook: withResponse()
Use withResponse() when you need final transport-layer customization right before dispatch.
Common use cases:
- Set headers
- Set status code
- Mutate final response body
- Apply framework-specific response behavior
Resource Example
import { ServerResponse, Resource } from 'resora';
class UserResource extends Resource {
withResponse(response: ServerResponse) {
response.header('X-Resource', 'user').setStatusCode(202);
const body = this.getBody();
this.setBody({
...body,
meta: {
...(body.meta || {}),
fromWithResponse: true,
},
});
}
}Collection Example
import { ServerResponse, ResourceCollection } from 'resora';
class UserCollection extends ResourceCollection {
withResponse(response: ServerResponse) {
response.header('X-Collection', 'users');
}
}Hook Context
Inside withResponse(), the framework-aware context is available as:
this.withResponseContext.response: ResoraServerResponsehelperthis.withResponseContext.raw: underlying Express/H3 response object
This hook runs immediately before the response is dispatched in both:
- Promise/await flow (
return await new Resource(...)) - Explicit response flow (
resource.response(...).header(...))
Design Rules When Writing Resources
- Always override
data()when extending. - Use
this.propertyto access original data fields. - Use
this.toArray()inside collections to transform all items. - Define
collectswhen extendingResourceCollection. - Let metadata extraction remain automatic.