A lightweight, type-safe resource transformation library for TypeScript, inspired by Laravel's API Resources. Easily transform your data structures into JSON:API-compatible responses with support for collections, pagination, and conditional attributes.
- π Type-safe transformations with TypeScript
- π Collections & Pagination out of the box
- π Conditional attributes based on runtime conditions
- π§© Composable resources with
mergeandwhenmethods - π‘οΈ Null-safe handling throughout
- π Flexible metadata for collections and pagination
bun add @elysion/resource
# or
npm install @elysion/resource
# or
yarn add @elysion/resourceimport { defineResource } from '@elysion/resource';
interface User {
id: number;
username: string;
email: string;
isAdmin: boolean;
posts: Array<{ id: number; title: string }>;
}
const UserResource = defineResource<User, {
id: number;
name: string;
email: string;
is_admin: boolean;
post_count: number;
latest_post?: { id: number; title: string } | null;
}>({
id: (user) => user.id,
name: (user) => user.username,
email: (user) => user.email,
is_admin: (user) => user.isAdmin,
post_count: (user) => user.posts?.length ?? 0,
latest_post: (user) => user.posts[0] ?? null,
});const user = {
id: 1,
username: 'johndoe',
email: 'john@example.com',
isAdmin: true,
posts: [
{ id: 1, title: 'Hello World' },
{ id: 2, title: 'Getting Started' }
]
};
const transformed = UserResource(user);
/*
{
id: 1,
name: 'johndoe',
email: 'john@example.com',
is_admin: true,
post_count: 2,
latest_post: { id: 1, title: 'Hello World' }
}
*/const users = [
{ id: 1, username: 'alice', email: 'alice@example.com', isAdmin: false, posts: [] },
{ id: 2, username: 'bob', email: 'bob@example.com', isAdmin: true, posts: [{ id: 1, title: 'Hi' }] },
null // null values are handled gracefully
];
const collection = UserResource.collection(users);
/*
{
data: [
{
id: 1,
name: 'alice',
email: 'alice@example.com',
is_admin: false,
post_count: 0,
latest_post: null
},
{
id: 2,
name: 'bob',
email: 'bob@example.com',
is_admin: true,
post_count: 1,
latest_post: { id: 1, title: 'Hi' }
},
null
],
meta: {
count: 3,
valid_count: 2
}
}
*/const items = Array(15).fill(0).map((_, i) => ({
id: i + 1,
username: `user${i + 1}`,
email: `user${i + 1}@example.com`,
isAdmin: false,
posts: []
}));
const paginated = UserResource.paginate(
items,
45, // total items
2, // current page
15, // per page
undefined, // context (optional)
{ custom_meta: 'value' } // additional metadata (optional)
);
/*
{
data: [
// ... 15 user objects
],
meta: {
current_page: 2,
from: 16,
last_page: 3,
per_page: 15,
to: 30,
total: 45,
valid_count: 15,
custom_meta: 'value'
}
}
*/const AdminResource = defineResource<User, {
id: number;
email: string;
is_admin: boolean;
admin_only?: string;
}>({
id: (user) => user.id,
email: (user) => user.email,
is_admin: (user) => user.isAdmin,
admin_only: (user) => 'Sensitive admin data'
});
// Only include admin_only field for admin users
const SecureUserResource = AdminResource
.when(
(user) => user.isAdmin,
{ admin_only: (user) => 'Sensitive admin data' }
);
// For non-admin users, admin_only will be undefined
const regularUser = { id: 1, username: 'user', email: 'user@example.com', isAdmin: false, posts: [] };
SecureUserResource(regularUser); // { id: 1, email: 'user@example.com', is_admin: false }const BaseResource = defineResource<User, { id: number; type: string }>({
id: (user) => user.id,
type: () => 'user'
});
const ExtendedResource = BaseResource.merge({
name: (user) => user.username,
email: (user) => user.email
});
ExtendedResource({ id: 1, username: 'test', email: 'test@example.com', isAdmin: false, posts: [] });
/*
{
id: 1,
type: 'user',
name: 'test',
email: 'test@example.com'
}
*/Pass additional context to your transformers:
const UserWithContextResource = defineResource<
User,
{ id: number; name: string; is_current_user: boolean }
>({
id: (user) => user.id,
name: (user) => user.username,
is_current_user: (user, context) => user.id === context?.currentUserId
});
const user = { id: 1, username: 'test', email: 'test@example.com', isAdmin: false, posts: [] };
UserWithContextResource(user, { currentUserId: 2 }); // { id: 1, name: 'test', is_current_user: false }Creates a new resource transformer with the given schema.
schema: An object where keys are output field names and values are either:- A static value
- A function that takes the model and optional context and returns the value
A function that can transform a model of type T into an object of type R.
The returned function has additional methods:
.collection(models, context?, meta?): Transform an array of models.paginate(items, total, currentPage, perPage, context?, meta?): Create a paginated response.when(condition, attributes): Conditionally include attributes.merge(attributes): Merge additional attributes into the schema
MIT
bun run devOpen http://localhost:3000/ with your browser to see the result.