npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@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

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-generator

Usage

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 item
  • POST /api/resource - Create a new item
  • PUT /api/resource/:id - Update an item
  • DELETE /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 metadata

Response 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:

  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

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 id field 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=active

Paginated 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 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:

{
  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 payloads
  • express.urlencoded({ extended: true }) - For parsing URL-encoded data

This means you can send data in your requests using either:

  1. JSON format:
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "John Doe", "email": "[email protected]"}'
  1. 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 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:

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=john

Comparison 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=inactive

Array Operations

# Value in array (comma-separated values)
GET /api/users?status__in=active,pending

# Value not in array
GET /api/users?status__nin=inactive,deleted

Combining Multiple Conditions

You can combine multiple conditions in a single request:

GET /api/users?age__gte=18&status__in=active,pending&name__ilike=john

Global 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=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.

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

  1. 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();
};
  1. 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();
};
  1. 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();
};
  1. 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'])]
}
  1. 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

  1. Order Matters: Middleware executes in the order they are defined. Put authentication before validation.
middleware: {
  create: [authMiddleware, validationMiddleware]; // Auth first, then validation
}
  1. 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" });
  }
};
  1. 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 route
  • handler: The route handler function
  • middleware: 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

  1. Route Naming: Use kebab-case for route paths
routes: {
  "verify-email": { ... },
  "reset-password": { ... },
  "change-password": { ... }
}
  1. 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]
  }
}
  1. 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);
}
  1. Route Parameters: Use URL parameters for resource identifiers
routes: {
  "verify-email/:token": { ... },
  "reset-password/:token": { ... },
  "users/:id/profile": { ... }
}
  1. 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