@franshiro/api-generator
v0.1.8
Published
Express REST API generator for Sequelize, Mongoose, and raw SQL models with attribute control, pagination, and custom handlers
Maintainers
Readme
API Generator
A Node.js package for automatically generating REST API endpoints from your models using Express.js.
Features
- Support for multiple model types:
- Sequelize (SQL ORM)
- Mongoose (MongoDB ODM) - Coming soon
- Raw SQL queries - Coming soon
- Automatic CRUD endpoint generation
- TypeScript support
- Extensible architecture for custom generators
- Custom logger support
- Automatic body parsing for JSON and URL-encoded data
- Built-in pagination support - Automatic pagination with metadata
Installation
npm i @franshiro/api-generatorUsage
import express from "express";
import { generateApi } from "api-generator";
import { User } from "./models/User"; // Your Sequelize model
// Custom logger implementation
const customLogger = {
info: (message: string, meta?: any) => {
console.log(`[INFO] ${message}`, meta);
},
error: (message: string, meta?: any) => {
console.error(`[ERROR] ${message}`, meta);
},
warn: (message: string, meta?: any) => {
console.warn(`[WARN] ${message}`, meta);
},
debug: (message: string, meta?: any) => {
console.debug(`[DEBUG] ${message}`, meta);
},
};
const app = express();
// Note: You don't need to add express.json() middleware
// as it's automatically added by the package
generateApi(app, {
basePath: "/api",
resources: [
{
name: "users",
model: User,
type: "sequelize",
options: {
pagination: true,
auth: true,
// Control which attributes are displayed
attributes: {
list: ["id", "name", "email", "createdAt"], // Only show basic info in list
get: { exclude: ["password", "resetToken"] }, // Hide sensitive data in detail view
},
// Searchable fields
searchableFields: ["name", "email"],
// Default includes for relationships
defaultIncludes: [
{
model: "Role",
as: "role",
attributes: ["id", "name"],
},
],
},
},
],
options: {
logger: customLogger,
},
});
app.listen(3000, () => {
console.log("Server running on port 3000");
});Generated Endpoints
For each resource, the following endpoints are automatically generated:
GET /api/resource- List all items (supports pagination, search, filtering)GET /api/resource/:id- Get a single itemPOST /api/resource- Create a new itemPUT /api/resource/:id- Update an itemDELETE /api/resource/:id- Delete an item
Endpoint Examples with Pagination
# Basic list request (with pagination enabled)
GET /api/users
# Returns: paginated response with 10 items by default
# Paginated request
GET /api/users?page=2&limit=20
# Returns: page 2 with 20 items
# With search and filtering
GET /api/users?page=1&limit=10&search=john&sortBy=createdAt&sortOrder=DESC
# Returns: filtered and sorted results with pagination
# Field-specific filtering
GET /api/users?page=1&status=active&role__in=admin,manager
# Returns: filtered by status and role with pagination metadataResponse Format
With Pagination Enabled:
{
"status": "success",
"message": "Successfully fetched users",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"createdAt": "2023-01-01T00:00:00.000Z"
}
],
"pagination": {
"total": 150,
"page": 1,
"limit": 10,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
}
}Without Pagination:
{
"status": "success",
"message": "Successfully fetched users",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"createdAt": "2023-01-01T00:00:00.000Z"
}
]
}Configuration
Resource Options
interface ResourceOptions {
basePath?: string; // Custom base path for the resource
middleware?: any[]; // Custom middleware
pagination?: boolean; // Enable pagination
auth?: boolean; // Enable authentication
roles?: string[]; // Role-based access control
defaultIncludes?: IncludeOptions[]; // Default includes for relationships
searchableFields?: string[]; // Fields to search in
attributes?: {
list?: string[] | { include?: string[]; exclude?: string[] };
get?: string[] | { include?: string[]; exclude?: string[] };
};
handlers?: {
list?: (req: Request, res: Response) => Promise<void>;
get?: (req: Request, res: Response) => Promise<void>;
create?: (req: Request, res: Response) => Promise<void>;
update?: (req: Request, res: Response) => Promise<void>;
delete?: (req: Request, res: Response) => Promise<void>;
};
}Custom Handlers
You can provide your own handlers for any CRUD operation. If a custom handler is provided, it will be used instead of the default implementation.
Example with custom handlers:
import express from "express";
import { generateApi } from "api-generator";
import { User } from "./models/User";
const app = express();
generateApi(app, {
basePath: "/api",
resources: [
{
name: "users",
model: User,
type: "sequelize",
options: {
pagination: true,
handlers: {
// Custom list handler
list: async (req, res) => {
// Your custom implementation
const users = await User.findAll({
where: { status: "active" },
order: [["createdAt", "DESC"]],
});
res.json(users);
},
// Custom create handler
create: async (req, res) => {
// Your custom implementation
const user = await User.create({
...req.body,
status: "active",
createdAt: new Date(),
});
res.status(201).json(user);
},
// Custom update handler
update: async (req, res) => {
const { id } = req.params;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Your custom implementation
await user.update({
...req.body,
updatedAt: new Date(),
});
res.json(user);
},
},
},
},
],
});When using custom handlers:
- The handler will receive the standard Express
reqandresobjects - You have full control over the implementation
- You can still access the model and other resources
- You can implement custom business logic, validation, or error handling
- You can modify the response format
Attribute Control
You can control which attributes are displayed in the list and get operations using the attributes option. This is useful for:
- Hiding sensitive data (like passwords)
- Reducing response payload size
- Controlling data exposure based on user roles
- Optimizing performance by selecting only needed fields
Configuration Options
attributes?: {
list?: string[] | { include?: string[]; exclude?: string[] };
get?: string[] | { include?: string[]; exclude?: string[] };
}Examples
1. Include specific attributes only:
{
name: "users",
model: User,
type: "sequelize",
options: {
attributes: {
list: ['id', 'name', 'email', 'createdAt'],
get: ['id', 'name', 'email', 'phone', 'address', 'createdAt', 'updatedAt']
}
}
}2. Exclude specific attributes:
{
name: "users",
model: User,
type: "sequelize",
options: {
attributes: {
list: { exclude: ['password', 'resetToken', 'resetTokenExpiry'] },
get: { exclude: ['password'] }
}
}
}3. Include specific attributes (alternative syntax):
{
name: "users",
model: User,
type: "sequelize",
options: {
attributes: {
list: { include: ['id', 'name', 'email'] },
get: { include: ['id', 'name', 'email', 'phone', 'address'] }
}
}
}4. Different attributes for different operations:
{
name: "users",
model: User,
type: "sequelize",
options: {
attributes: {
// List shows minimal info
list: ['id', 'name', 'email'],
// Get shows full details except password
get: { exclude: ['password', 'resetToken'] }
}
}
}Behavior
- Array of strings: Only the specified attributes will be included
- Object with
include: Only the specified attributes will be included - Object with
exclude: All attributes except the specified ones will be included - Not specified: All attributes will be included (default behavior)
Notes
- Attribute filtering only applies to the main model, not to included associations
- Invalid attribute names are automatically filtered out
- The
idfield is always included if it exists in the model - This feature works with both paginated and non-paginated responses
Pagination
The API Generator includes built-in pagination support for list operations. When enabled, it automatically handles pagination parameters and returns paginated responses with metadata.
Enabling Pagination
{
name: "users",
model: User,
type: "sequelize",
options: {
pagination: true, // Enable pagination
attributes: {
list: ['id', 'name', 'email', 'createdAt']
}
}
}Query Parameters
When pagination is enabled, the following query parameters are available:
page- Page number (default: 1)limit- Number of items per page (default: 10, max: 100)sortBy- Field to sort by (default: 'id')sortOrder- Sort order: 'ASC' or 'DESC' (default: 'ASC')
Example Requests
# Get first page with default settings
GET /api/users
# Get page 2 with 20 items per page
GET /api/users?page=2&limit=20
# Sort by name in descending order
GET /api/users?sortBy=name&sortOrder=DESC&page=1&limit=15
# Combine with search and filtering
GET /api/users?page=1&limit=10&search=john&status=activePaginated Response Format
When pagination is enabled, the response includes both data and pagination metadata:
{
"status": "success",
"message": "Successfully fetched users",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "[email protected]",
"createdAt": "2023-01-01T00:00:00.000Z"
}
// ... more users
],
"pagination": {
"total": 150,
"page": 1,
"limit": 10,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
}
}Pagination Metadata
total- Total number of items in the databasepage- Current page numberlimit- Number of items per pagetotalPages- Total number of pageshasNext- Whether there is a next pagehasPrev- Whether there is a previous page
Custom Pagination Configuration
You can customize pagination behavior by implementing custom handlers:
{
name: "users",
model: User,
type: "sequelize",
options: {
pagination: true,
handlers: {
list: async (req, res) => {
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 10, 50); // Custom max limit
const offset = (page - 1) * limit;
const { count, rows } = await User.findAndCountAll({
limit,
offset,
order: [['createdAt', 'DESC']],
where: { status: 'active' } // Custom filtering
});
const totalPages = Math.ceil(count / limit);
res.json({
statusMessage: "Users retrieved successfully",
data: rows,
pagination: {
total: count,
page,
limit,
totalPages,
hasNext: page < totalPages,
hasPrev: page > 1
}
});
}
}
}
}Notes
- Pagination is disabled by default
- When pagination is disabled, all items are returned in a single response
- The maximum limit per page is 100 to prevent performance issues
- Pagination works seamlessly with search, filtering, and attribute control features
- Custom handlers receive pagination support automatically when returning data with pagination metadata
Custom Logger
You can provide your own logger implementation by implementing the Logger interface:
interface Logger {
info(message: string, meta?: any): void;
error(message: string, meta?: any): void;
warn(message: string, meta?: any): void;
debug(message: string, meta?: any): void;
}Example with Winston:
import winston from "winston";
const winstonLogger = winston.createLogger({
// Your Winston configuration
});
const customLogger = {
info: (message: string, meta?: any) => winstonLogger.info(message, meta),
error: (message: string, meta?: any) => winstonLogger.error(message, meta),
warn: (message: string, meta?: any) => winstonLogger.warn(message, meta),
debug: (message: string, meta?: any) => winstonLogger.debug(message, meta),
};Body Parsing
The package automatically adds the following middleware for parsing request bodies:
express.json()- For parsing JSON payloadsexpress.urlencoded({ extended: true })- For parsing URL-encoded data
This means you can send data in your requests using either:
- JSON format:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "[email protected]"}'- URL-encoded format:
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "name=John%20Doe&email=john%40example.com"Pagination
When pagination is enabled for a resource, the list endpoint (GET /api/resource) supports the following query parameters:
page(default: 1) - The page number to fetchlimit(default: 10, max: 100) - Number of items per pagesortBy(default: 'id') - Field to sort bysortOrder(default: 'ASC') - Sort order ('ASC' or 'DESC')
Example request with pagination:
curl "http://localhost:3000/api/users?page=2&limit=20&sortBy=createdAt&sortOrder=DESC"The response will include both the data and pagination metadata:
{
"data": [...],
"pagination": {
"total": 100,
"page": 2,
"limit": 20,
"totalPages": 5,
"hasNext": true,
"hasPrev": true
}
}Search and Filtering
The list endpoint (GET /api/resource) supports various search and filtering operations through query parameters. Here are the available operators:
Basic Search
# Exact match
GET /api/users?name=John
# Pattern matching (case insensitive)
GET /api/users?name__ilike=johnComparison Operators
# Greater than
GET /api/users?age__gt=18
# Greater than or equal
GET /api/users?age__gte=18
# Less than
GET /api/users?age__lt=30
# Less than or equal
GET /api/users?age__lte=30
# Not equal
GET /api/users?status__neq=inactiveArray Operations
# Value in array (comma-separated values)
GET /api/users?status__in=active,pending
# Value not in array
GET /api/users?status__nin=inactive,deletedCombining Multiple Conditions
You can combine multiple conditions in a single request:
GET /api/users?age__gte=18&status__in=active,pending&name__ilike=johnGlobal Search
If you've configured searchableFields in your resource options, you can use the search parameter to search across multiple fields:
GET /api/users?search=johnThis will search for "john" in all configured searchable fields.
Middleware Support
You can add middleware to specific routes using the middleware option. This allows you to apply different middleware to different routes of the same resource.
Example with Middleware
import express from "express";
import { generateApi } from "api-generator";
import { User } from "./models/User";
// Example middleware functions
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
// Check if user is authenticated
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: "Unauthorized" });
}
// Add user to request
req.user = { id: 1, role: "admin" };
next();
};
const validationMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => {
// Validate request body
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: "Name and email are required" });
}
next();
};
const loggingMiddleware = (req: Request, res: Response, next: NextFunction) => {
console.log(`${new Date().toISOString()} - ${req.method} ${req.path}`);
next();
};
const app = express();
generateApi(app, {
basePath: "/api",
resources: [
{
name: "users",
model: User,
type: "sequelize",
options: {
pagination: true,
// Apply middleware to specific routes
middleware: {
list: [loggingMiddleware], // Only for GET /api/users
create: [authMiddleware, validationMiddleware], // For POST /api/users
update: [authMiddleware, validationMiddleware], // For PUT /api/users/:id
delete: [authMiddleware], // For DELETE /api/users/:id
get: [loggingMiddleware], // For GET /api/users/:id
},
handlers: {
// Your custom handlers here
},
},
},
],
});Middleware Types
The middleware option supports the following route-specific middleware:
interface MiddlewareOptions {
list?: ((req: Request, res: Response, next: NextFunction) => void)[]; // GET /api/resource
get?: ((req: Request, res: Response, next: NextFunction) => void)[]; // GET /api/resource/:id
create?: ((req: Request, res: Response, next: NextFunction) => void)[]; // POST /api/resource
update?: ((req: Request, res: Response, next: NextFunction) => void)[]; // PUT /api/resource/:id
delete?: ((req: Request, res: Response, next: NextFunction) => void)[]; // DELETE /api/resource/:id
}Common Middleware Use Cases
- Authentication
const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: "Unauthorized" });
}
// Verify token and add user to request
req.user = verifyToken(token);
next();
};- Validation
const validateUser = (req: Request, res: Response, next: NextFunction) => {
const { name, email, age } = req.body;
const errors = [];
if (!name) errors.push("Name is required");
if (!email) errors.push("Email is required");
if (age && (age < 0 || age > 120)) errors.push("Invalid age");
if (errors.length > 0) {
return res.status(400).json({ message: errors.join(", ") });
}
next();
};- Logging
const requestLogger = (req: Request, res: Response, next: NextFunction) => {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`
);
});
next();
};- Role-based Access Control
const roleMiddleware = (roles: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ message: 'Unauthorized' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
};
};
// Usage
middleware: {
create: [authMiddleware, roleMiddleware(['admin'])],
update: [authMiddleware, roleMiddleware(['admin', 'manager'])],
delete: [authMiddleware, roleMiddleware(['admin'])]
}- Rate Limiting
import rateLimit from 'express-rate-limit';
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
// Usage
middleware: {
list: [apiLimiter],
create: [apiLimiter]
}Best Practices
- Order Matters: Middleware executes in the order they are defined. Put authentication before validation.
middleware: {
create: [authMiddleware, validationMiddleware]; // Auth first, then validation
}- Error Handling: Always use proper error responses in middleware
const errorMiddleware = (req: Request, res: Response, next: NextFunction) => {
try {
// Your middleware logic
next();
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
};- Reusable Middleware: Create reusable middleware functions
const validateFields = (fields: string[]) => {
return (req: Request, res: Response, next: NextFunction) => {
const missing = fields.filter(field => !req.body[field]);
if (missing.length > 0) {
return res.status(400).json({
message: `Missing required fields: ${missing.join(', ')}`
});
}
next();
};
};
// Usage
middleware: {
create: [authMiddleware, validateFields(['name', 'email'])],
update: [authMiddleware, validateFields(['name'])]
}Custom Routes
You can add custom routes to your resources using the routes option. This allows you to add endpoints beyond the standard CRUD operations.
Example with Custom Routes
import express from "express";
import { generateApi } from "api-generator";
import { User } from "./models/User";
const app = express();
generateApi(app, {
basePath: "/api",
resources: [
{
name: "users",
model: User,
type: "sequelize",
options: {
// Custom routes
routes: {
// Login endpoint
login: {
method: "post",
handler: async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ where: { email } });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ message: "Invalid credentials" });
}
const token = generateToken(user);
res.json({ token, user });
},
middleware: [rateLimiter], // Optional middleware
},
// Verify email endpoint
"verify-email/:token": {
method: "get",
handler: async (req, res) => {
const { token } = req.params;
const user = await User.findOne({
where: { verificationToken: token },
});
if (!user) {
return res
.status(400)
.json({ message: "Invalid verification token" });
}
await user.update({ verified: true, verificationToken: null });
res.json({ message: "Email verified successfully" });
},
},
// Change password endpoint
"change-password": {
method: "post",
handler: async (req, res) => {
const { currentPassword, newPassword } = req.body;
const user = await User.findByPk(req.user.id);
if (!(await user.comparePassword(currentPassword))) {
return res
.status(401)
.json({ message: "Current password is incorrect" });
}
await user.update({ password: newPassword });
res.json({ message: "Password changed successfully" });
},
middleware: [authMiddleware], // Require authentication
},
},
},
},
],
});Custom Route Configuration
Each custom route is defined with the following properties:
interface CustomRoute {
method: "get" | "post" | "put" | "delete" | "patch";
handler: (req: Request, res: Response) => Promise<void>;
middleware?: ((req: Request, res: Response, next: NextFunction) => void)[];
}method: The HTTP method for the routehandler: The route handler functionmiddleware: Optional array of middleware functions
Response Format
Custom routes automatically use the standard response format:
Success Response:
{
"status": "success",
"message": "Success",
"data": { ... }
}Error Response:
{
"status": "error",
"message": "Error message"
}Best Practices
- Route Naming: Use kebab-case for route paths
routes: {
"verify-email": { ... },
"reset-password": { ... },
"change-password": { ... }
}- Middleware Organization: Group related middleware
const authRoutes = {
"login": {
method: "post",
handler: loginHandler,
middleware: [rateLimiter]
},
"logout": {
method: "post",
handler: logoutHandler,
middleware: [authMiddleware]
}
};
// Usage
routes: {
...authRoutes,
"profile": {
method: "get",
handler: profileHandler,
middleware: [authMiddleware]
}
}- Error Handling: Use consistent error responses
const errorHandler = (error: Error) => {
if (error instanceof ValidationError) {
return res.status(400).json({ message: error.message });
}
if (error instanceof AuthError) {
return res.status(401).json({ message: error.message });
}
return res.status(500).json({ message: "Internal server error" });
};
// Usage in route handler
try {
// Your logic here
} catch (error) {
return errorHandler(error);
}- Route Parameters: Use URL parameters for resource identifiers
routes: {
"verify-email/:token": { ... },
"reset-password/:token": { ... },
"users/:id/profile": { ... }
}- Query Parameters: Use query parameters for filtering and pagination
routes: {
"search": {
method: "get",
handler: async (req, res) => {
const { query, page = 1, limit = 10 } = req.query;
// Your search logic here
}
}
}License
MIT
