api-resource-transformer
v1.0.0
Published
Framework-agnostic API Resource classes for Node.js - Transform your models like Laravel Eloquent API Resources
Maintainers
Readme
API Resource Transformer
Framework-agnostic API Resource classes for Node.js - Transform your models like Laravel Eloquent API Resources.
Works with any Node.js framework and any database/ORM.
Features
- 🚀 Framework-agnostic - Works with Express, Fastify, NestJS, Koa, AdonisJS
- 🗄️ Database-agnostic - Works with Mongoose, Prisma, Sequelize, Knex, Drizzle, or plain objects
- 📦 Zero dependencies - Pure JavaScript, no external packages required
- 🔄 Transform data - Rename fields, hide sensitive data, add computed fields
- 🔗 Nested resources - Include related models seamlessly
- 📝 TypeScript support - Full TypeScript declarations included
- ⚡ Lightweight - Minimal overhead, maximum flexibility
Installation
npm install api-resource-transformerQuick Start
Basic Usage
const Resource = require('api-resource-transformer');
class UserResource extends Resource {
// Define fields to include
fields() {
return {
id: 'id',
name: 'name',
email: 'email',
createdAt: 'created_at',
};
}
// Hide sensitive fields
hidden() {
return ['password', 'remember_token'];
}
// Add computed fields
computed() {
return {
full_name: this.data.name || '',
initials: this.getInitials(),
};
}
getInitials() {
const names = this.data.name.split(' ');
return names.map(n => n[0]).join('').toUpperCase();
}
}
// Transform single resource
const user = { id: 1, name: 'John Doe', email: '[email protected]', password: 'secret' };
const transformed = UserResource.toJSON(user);
// Result: { id: 1, name: 'John Doe', email: '[email protected]', created_at: ..., full_name: 'John Doe', initials: 'JD' }
// Transform collection
const users = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
const collection = UserResource.collection(users);API Reference
Static Methods
Resource.toJSON(record)
Transform a single record to JSON.
const resource = UserResource.toJSON(user);Resource.collection(records)
Transform an array of records to JSON array.
const resources = UserResource.collection(users);Instance Methods (Override in Child Classes)
fields()
Define which fields to include and optionally rename them.
fields() {
return {
id: 'id', // Keep original name
firstName: 'first_name', // Rename field
email: 'email',
};
}computed()
Add computed/calculated fields. Can return an object or a function.
// Return object
computed() {
return {
full_name: `${this.data.firstName} ${this.data.lastName}`,
age: new Date().getFullYear() - this.data.birthYear,
};
}
// Return function (more flexible)
computed() {
return (data) => {
return {
full_name: `${data.firstName} ${data.lastName}`,
};
};
}relations()
Include nested/related resources.
const PostResource = require('./PostResource');
class UserResource extends Resource {
relations() {
return {
posts: PostResource.collection(this.data.posts),
profile: ProfileResource.toJSON(this.data.profile),
};
}
}hidden()
Fields to exclude from output.
hidden() {
return ['password', 'remember_token', 'deleted_at'];
}only()
Whitelist - only include these fields (overrides fields()).
only() {
return ['id', 'name', 'email']; // Only these fields will be included
}Framework Examples
Express.js
const express = require('express');
const UserResource = require('./resources/UserResource');
const app = express();
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
return res.json({
success: true,
data: UserResource.toJSON(user),
});
});
app.get('/api/users', async (req, res) => {
const users = await User.findAll();
return res.json({
success: true,
data: UserResource.collection(users),
});
});Fastify
const fastify = require('fastify');
const UserResource = require('./resources/UserResource');
app.get('/api/users', async (request, reply) => {
const users = await User.findAll();
return {
success: true,
data: UserResource.collection(users),
};
});NestJS
import { Controller, Get } from '@nestjs/common';
import { Resource } from 'api-resource-transformer';
import { UserResource } from './resources/UserResource';
@Controller('users')
export class UsersController {
@Get()
async findAll() {
const users = await this.userService.findAll();
return {
success: true,
data: UserResource.collection(users),
};
}
}Koa
const Router = require('@koa/router');
const UserResource = require('./resources/UserResource');
router.get('/api/users', async (ctx) => {
const users = await User.findAll();
ctx.body = {
success: true,
data: UserResource.collection(users),
};
});Database/ORM Examples
Mongoose (MongoDB)
const user = await User.findById(id)
.populate('posts')
.lean(); // Important: .lean() returns plain object
return UserResource.toJSON(user);Prisma
const user = await prisma.user.findUnique({
where: { id },
include: { posts: true },
});
return UserResource.toJSON(user); // Prisma returns plain objectsSequelize
const user = await User.findByPk(id, {
include: [{ model: Post }],
});
return UserResource.toJSON(user.toJSON()); // Convert to plain objectKnex / Raw SQL
const users = await knex('users').select('*');
return UserResource.collection(users); // Already plain objectsDrizzle ORM
const users = await db.select().from(users);
return UserResource.collection(users); // Already plain objectsComplete Example
const Resource = require('api-resource-transformer');
const PostResource = require('./PostResource');
class UserResource extends Resource {
fields() {
return {
id: 'id',
name: 'name',
email: 'email',
createdAt: 'created_at',
};
}
computed() {
return {
full_name: this.data.name || '',
initials: this.getInitials(),
is_active: this.data.status === 'active',
};
}
getInitials() {
const names = this.data.name.split(' ');
return names.map(n => n[0]).join('').toUpperCase();
}
relations() {
return {
posts: this.data.posts ? PostResource.collection(this.data.posts) : null,
};
}
hidden() {
return ['password', 'remember_token'];
}
}
// Usage
const user = {
id: 1,
name: 'John Doe',
email: '[email protected]',
password: 'secret',
status: 'active',
posts: [
{ id: 1, title: 'Post 1', content: '...' },
],
};
const transformed = UserResource.toJSON(user);
console.log(transformed);
// {
// id: 1,
// name: 'John Doe',
// email: '[email protected]',
// created_at: ...,
// full_name: 'John Doe',
// initials: 'JD',
// is_active: true,
// posts: [...]
// }TypeScript Support
import { Resource } from 'api-resource-transformer';
class UserResource extends Resource {
protected fields(): Record<string, string> {
return {
id: 'id',
name: 'name',
};
}
protected computed(): Record<string, any> {
return {
full_name: this.data.name,
};
}
}
const user = { id: 1, name: 'John' };
const transformed = UserResource.toJSON(user);Advanced Features
Dot Notation Support
Access nested fields using dot notation:
class UserResource extends Resource {
fields() {
return {
'profile.name': 'name',
'profile.bio': 'bio',
};
}
}Conditional Fields
computed() {
const computed = {};
if (this.data.role === 'admin') {
computed.admin_data = this.data.adminData;
}
return computed;
}Dynamic Relations
relations() {
const relations = {};
if (this.data.showPosts) {
relations.posts = PostResource.collection(this.data.posts);
}
return relations;
}Best Practices
- Keep Resources Simple: Each resource should represent one model/entity
- Reuse Resources: Use the same resource for related data (e.g., UserResource for author)
- Hide Sensitive Data: Always hide passwords, tokens, and sensitive fields
- Use Computed Fields: Calculate values that don't exist in the database
- Lazy Load Relations: Only include relations when needed to avoid N+1 queries
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
