API Resources
API Resources provide a transformation layer between your models and API responses. They give you fine-grained control over how data is serialized to JSON.
API Resources
API Resources provide a transformation layer between your models and API responses. They give you fine-grained control over how data is serialized to JSON.
Basic Usage
Create a resource by extending the Resource class:
import { Resource } from '@guren/core'
import type { User } from '../Models/User'
export class UserResource extends Resource<User> {
toArray() {
return {
id: this.resource.id,
name: this.resource.name,
email: this.resource.email,
createdAt: this.resource.createdAt?.toISOString(),
}
}
}
Using Resources in Controllers
import { Controller } from '@guren/core'
import { UserResource } from '../Resources/UserResource'
export default class UserController extends Controller {
async show(id: number) {
const user = await User.find(id)
return this.json({
data: new UserResource(user).toJSON(),
})
}
async index() {
const users = await User.all()
return this.json({
data: UserResource.collection(users),
})
}
}
Conditional Fields
Resources provide helper methods for conditionally including fields.
when()
Include a field only when a condition is true:
export class UserResource extends Resource<User> {
toArray() {
return {
id: this.resource.id,
name: this.resource.name,
// Only include email for verified users
email: this.when(this.resource.verified, this.resource.email),
// Use a callback for computed values
role: this.when(this.resource.isAdmin, () => 'admin'),
}
}
}
whenLoaded()
Include a field only when a relation is loaded:
export class PostResource extends Resource<Post> {
toArray() {
return {
id: this.resource.id,
title: this.resource.title,
// Only include if author relation is loaded
author: this.whenLoaded('author', () => ({
id: this.resource.author?.id,
name: this.resource.author?.name,
})),
// Include nested resource when loaded
comments: this.whenLoaded('comments', () =>
CommentResource.collection(this.resource.comments)
),
}
}
}
whenNotNull()
Include a field only if it's not null:
export class ProfileResource extends Resource<Profile> {
toArray() {
return {
id: this.resource.id,
bio: this.whenNotNull(this.resource.bio),
avatarUrl: this.whenNotNull(this.resource.avatarUrl),
}
}
}
whenOr()
Include a field with a default value:
export class SettingsResource extends Resource<Settings> {
toArray() {
return {
theme: this.whenOr(
this.resource.theme !== undefined,
this.resource.theme,
'light' // default value
),
}
}
}
Additional Data
Add extra data to the resource response:
const resource = new UserResource(user)
.additional({
permissions: ['read', 'write'],
meta: { version: '1.0' },
})
return this.json({ data: resource.toJSON() })
// { id: 1, name: 'John', ..., permissions: [...], meta: {...} }
Resource Collections
Transform an array of models into resources:
// Static method
const users = await User.all()
const data = UserResource.collection(users)
// Or use the collect helper
import { collect } from '@guren/core'
const data = collect(users, UserResource)
Pagination
Guren provides two pagination strategies.
Offset-Based Pagination
Traditional pagination with page numbers:
import { paginate, Paginator } from '@guren/core'
export default class UserController extends Controller {
async index() {
const page = Number(this.request.query('page') ?? 1)
const perPage = Number(this.request.query('per_page') ?? 15)
const result = await User.paginate({ page, perPage })
const paginator = paginate(result, {
path: '/api/users',
query: { per_page: String(result.meta.perPage) },
})
return this.json(paginator.toResource(UserResource))
}
}
Response format:
{
"data": [
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
],
"meta": {
"currentPage": 1,
"lastPage": 5,
"perPage": 15,
"total": 75,
"from": 1,
"to": 15
},
"links": {
"first": "/api/users?page=1&per_page=15",
"last": "/api/users?page=5&per_page=15",
"prev": null,
"next": "/api/users?page=2&per_page=15",
"pages": [
{ "page": 1, "url": "/api/users?page=1&per_page=15", "active": true },
{ "page": 2, "url": "/api/users?page=2&per_page=15", "active": false }
]
}
}
Cursor-Based Pagination
Ideal for infinite scroll and real-time data:
import { cursorPaginate, CursorPaginator } from '@guren/core'
export default class PostController extends Controller {
async index() {
const cursor = this.request.query('cursor')
const perPage = Number(this.request.query('per_page') ?? 20)
const posts = await Post.query()
.where('id', '>', decodeCursor(cursor) ?? 0)
.orderBy('id', 'asc')
.limit(perPage + 1)
.all()
const hasMore = posts.length > perPage
const items = hasMore ? posts.slice(0, perPage) : posts
const paginator = CursorPaginator.fromArray(items, cursor, perPage)
return this.json(paginator.toResource(PostResource))
}
}
Response format:
{
"data": [
{ "id": 101, "title": "Post 1" },
{ "id": 102, "title": "Post 2" }
],
"meta": {
"perPage": 20,
"nextCursor": "MTAy",
"prevCursor": null,
"hasMore": true
}
}
Paginator Methods
Offset Paginator
| Method | Description |
|---|---|
items() |
Get paginated items |
total() |
Get total item count |
perPage() |
Get items per page |
currentPage() |
Get current page number |
lastPage() |
Get last page number |
hasMorePages() |
Check if more pages exist |
onFirstPage() |
Check if on first page |
onLastPage() |
Check if on last page |
firstItem() |
Get first item index (1-based) |
lastItem() |
Get last item index (1-based) |
meta() |
Get pagination metadata |
links() |
Get pagination links |
withPath(path) |
Set base URL path |
withQuery(query) |
Add query parameters |
toResource(Class) |
Transform with resource class |
toJSON() |
Get raw paginated response |
Cursor Paginator
| Method | Description |
|---|---|
items() |
Get paginated items |
perPage() |
Get items per page |
currentCursor() |
Get current cursor |
nextCursor() |
Get next page cursor |
prevCursor() |
Get previous page cursor |
hasMorePages() |
Check if more pages exist |
meta() |
Get cursor pagination metadata |
toResource(Class) |
Transform with resource class |
JsonResource
For simple transformations without a custom class:
import { JsonResource } from '@guren/core'
const user = { id: 1, name: 'John', password: 'secret' }
const resource = new JsonResource(user)
// Returns: { id: 1, name: 'John', password: 'secret' }
Generating Resources
Use the CLI to generate a new resource:
bunx guren make:resource User
# Creates: app/Http/Resources/UserResource.ts
Best Practices
- Keep resources focused - One resource per model transformation
- Use whenLoaded for relations - Prevents N+1 issues by only including loaded relations
- Transform dates consistently - Use
.toISOString()for date fields - Hide sensitive data - Never expose passwords, tokens, or internal IDs
- Use cursor pagination for large datasets - Better performance than offset pagination