Express REST API generator for Sequelize, Mongoose, and raw SQL models with attribute control, pagination, and custom handlers
npm install @franshiro/api-generatorA Node.js package for automatically generating REST API endpoints from your models using Express.js.
- 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
``bash`
npm i @franshiro/api-generator
`typescript
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}
},
error: (message: string, meta?: any) => {
console.error(, meta);[WARN] ${message}
},
warn: (message: string, meta?: any) => {
console.warn(, meta);[DEBUG] ${message}
},
debug: (message: string, meta?: any) => {
console.debug(, 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");
});
`
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
`bashBasic list request (with pagination enabled)
GET /api/usersReturns: paginated response with 10 items by default
$3
With Pagination Enabled:
`json
{
"status": "success",
"message": "Successfully fetched users",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2023-01-01T00:00:00.000Z"
}
],
"pagination": {
"total": 150,
"page": 1,
"limit": 10,
"totalPages": 15,
"hasNext": true,
"hasPrev": false
}
}
`Without Pagination:
`json
{
"status": "success",
"message": "Successfully fetched users",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2023-01-01T00:00:00.000Z"
}
]
}
`Configuration
$3
`typescript
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;
get?: (req: Request, res: Response) => Promise;
create?: (req: Request, res: Response) => Promise;
update?: (req: Request, res: Response) => Promise;
delete?: (req: Request, res: Response) => Promise;
};
}
`$3
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:
`typescript
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:
1. The handler will receive the standard Express
req and res objects
2. You have full control over the implementation
3. You can still access the model and other resources
4. You can implement custom business logic, validation, or error handling
5. You can modify the response format$3
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
`typescript
attributes?: {
list?: string[] | { include?: string[]; exclude?: string[] };
get?: string[] | { include?: string[]; exclude?: string[] };
}
`#### Examples
1. Include specific attributes only:
`typescript
{
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:
`typescript
{
name: "users",
model: User,
type: "sequelize",
options: {
attributes: {
list: { exclude: ['password', 'resetToken', 'resetTokenExpiry'] },
get: { exclude: ['password'] }
}
}
}
`3. Include specific attributes (alternative syntax):
`typescript
{
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:
`typescript
{
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
id field is always included if it exists in the model
- This feature works with both paginated and non-paginated responses$3
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
`typescript
{
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
`bash
Get first page with default settings
GET /api/usersGet page 2 with 20 items per page
GET /api/users?page=2&limit=20Sort by name in descending order
GET /api/users?sortBy=name&sortOrder=DESC&page=1&limit=15Combine with search and filtering
GET /api/users?page=1&limit=10&search=john&status=active
`#### Paginated Response Format
When pagination is enabled, the response includes both data and pagination metadata:
`json
{
"status": "success",
"message": "Successfully fetched users",
"data": [
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"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 database
- page - Current page number
- limit - Number of items per page
- totalPages - Total number of pages
- hasNext - Whether there is a next page
- hasPrev - Whether there is a previous page#### Custom Pagination Configuration
You can customize pagination behavior by implementing custom handlers:
`typescript
{
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
$3
You can provide your own logger implementation by implementing the
Logger interface:`typescript
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:
`typescript
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 payloads
- express.urlencoded({ extended: true }) - For parsing URL-encoded dataThis means you can send data in your requests using either:
1. JSON format:
`bash
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com"}'
`2. URL-encoded format:
`bash
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 fetch
- limit (default: 10, max: 100) - Number of items per page
- sortBy (default: 'id') - Field to sort by
- sortOrder (default: 'ASC') - Sort order ('ASC' or 'DESC')Example request with pagination:
`bash
curl "http://localhost:3000/api/users?page=2&limit=20&sortBy=createdAt&sortOrder=DESC"
`The response will include both the data and pagination metadata:
`json
{
"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:$3
`bash
Exact match
GET /api/users?name=JohnPattern matching (case insensitive)
GET /api/users?name__ilike=john
`$3
`bash
Greater than
GET /api/users?age__gt=18Greater than or equal
GET /api/users?age__gte=18Less than
GET /api/users?age__lt=30Less than or equal
GET /api/users?age__lte=30Not equal
GET /api/users?status__neq=inactive
`$3
`bash
Value in array (comma-separated values)
GET /api/users?status__in=active,pendingValue not in array
GET /api/users?status__nin=inactive,deleted
`$3
You can combine multiple conditions in a single request:
`bash
GET /api/users?age__gte=18&status__in=active,pending&name__ilike=john
`$3
If you've configured
searchableFields in your resource options, you can use the search parameter to search across multiple fields:`bash
GET /api/users?search=john
`This 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.$3
`typescript
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
},
},
},
],
});
`$3
The middleware option supports the following route-specific middleware:
`typescript
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
}
`$3
1. Authentication
`typescript
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();
};
`2. Validation
`typescript
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();
};
`3. Logging
`typescript
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();
};
`4. Role-based Access Control
`typescript
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'])]
}
`5. Rate Limiting
`typescript
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]
}
`$3
1. Order Matters: Middleware executes in the order they are defined. Put authentication before validation.
`typescript
middleware: {
create: [authMiddleware, validationMiddleware]; // Auth first, then validation
}
`2. Error Handling: Always use proper error responses in middleware
`typescript
const errorMiddleware = (req: Request, res: Response, next: NextFunction) => {
try {
// Your middleware logic
next();
} catch (error) {
res.status(500).json({ message: "Internal server error" });
}
};
`3. Reusable Middleware: Create reusable middleware functions
`typescript
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.$3
`typescript
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
},
},
},
},
],
});
`$3
Each custom route is defined with the following properties:
`typescript
interface CustomRoute {
method: "get" | "post" | "put" | "delete" | "patch";
handler: (req: Request, res: Response) => Promise;
middleware?: ((req: Request, res: Response, next: NextFunction) => void)[];
}
`-
method: The HTTP method for the route
- handler: The route handler function
- middleware: Optional array of middleware functions$3
Custom routes automatically use the standard response format:
Success Response:
`json
{
"status": "success",
"message": "Success",
"data": { ... }
}
`Error Response:
`json
{
"status": "error",
"message": "Error message"
}
`$3
1. Route Naming: Use kebab-case for route paths
`typescript
routes: {
"verify-email": { ... },
"reset-password": { ... },
"change-password": { ... }
}
`2. Middleware Organization: Group related middleware
`typescript
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]
}
}
`3. Error Handling: Use consistent error responses
`typescript
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);
}
`4. Route Parameters: Use URL parameters for resource identifiers
`typescript
routes: {
"verify-email/:token": { ... },
"reset-password/:token": { ... },
"users/:id/profile": { ... }
}
`5. Query Parameters: Use query parameters for filtering and pagination
`typescript
routes: {
"search": {
method: "get",
handler: async (req, res) => {
const { query, page = 1, limit = 10 } = req.query;
// Your search logic here
}
}
}
``MIT