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 🙏

© 2026 – Pkg Stats / Ryan Hefner

crud-api-express

v2.0.0

Published

A powerful, flexible CRUD controller for Express + Mongoose — auto-generates RESTful endpoints with lifecycle hooks, validation, soft delete, search, bulk operations, pagination, and more.

Downloads

168

Readme

crud-api-express

npm downloads license made-with-node made-with-typescript express MongoDB

A powerful, flexible CRUD controller for Express + Mongoose — auto‑generates RESTful endpoints with lifecycle hooks, validation, soft delete, search, bulk operations, pagination metadata, and more.


✨ Features

  • 🚀 Zero boilerplate — full CRUD in 3 lines of code
  • 🪝 Lifecycle hooksbeforeCreate, afterUpdate, beforeDelete, etc.
  • Validation hooks — reject bad data before it hits Mongoose
  • 🔍 Search endpoint — case-insensitive text search across multiple fields
  • 🗑️ Soft delete — mark records as deleted + restore endpoint
  • 📦 Bulk operations — create, update, and delete in batch
  • 🔒 Per-route middleware — different auth/logic for read vs. write
  • 📄 Pagination metadata — total, pages, hasNext, hasPrev
  • 🎯 Field selection & population?select=name,email&populate=author
  • 🔢 Count & exists — lightweight endpoints for checking data
  • 📊 Dynamic aggregation — static pipelines or functions of req
  • 🏗️ PATCH support — partial updates with $set semantics
  • 🔗 Related model cascading — auto‑create/update/delete linked models
  • 📝 Full TypeScript support — exported types, generics, JSDoc

📦 Installation

npm install crud-api-express
# Peer dependencies (install alongside):
npm install express mongoose

🚀 Quick Start

import express from 'express';
import mongoose from 'mongoose';
import CrudController from 'crud-api-express';

// 1. Define your model
const UserSchema = new mongoose.Schema({
  name:  { type: String, required: true },
  email: { type: String, required: true, unique: true },
  role:  { type: String, default: 'user', enum: ['user', 'admin'] },
}, { timestamps: true, versionKey: false });

const User = mongoose.model('User', UserSchema);

// 2. Create the controller
const userCtrl = new CrudController(User, 'users');

// 3. Mount and go
const app = express();
app.use(express.json());
app.use('/api', userCtrl.getRouter());

mongoose.connect('mongodb://localhost:27017/mydb').then(() => {
  app.listen(3000, () => console.log('Server running on port 3000'));
});

That's it — you now have 15+ endpoints auto-generated. 🎉


🛣️ Auto-Generated Endpoints

| Method | Endpoint | Description | |--------|----------|-------------| | POST | /users | Create a record | | POST | /users/bulk | Bulk create records | | GET | /users | List all (filter/sort/paginate/select/populate) | | GET | /users/:id | Get one by ID | | GET | /users/search | Text search across fields | | GET | /users/count | Count matching records | | GET | /users/exists/:id | Check if a record exists | | GET | /users/aggregate | Run aggregation pipeline | | PUT | /users/:id | Full update by ID | | PATCH | /users/:id | Partial update by ID | | PATCH | /users/bulk | Bulk update by filter | | PATCH | /users/:id/restore | Restore soft-deleted record (soft delete only) | | DELETE | /users/:id | Delete one by ID | | DELETE | /users | Delete by filter | | DELETE | /users/bulk | Bulk delete by IDs array |


⚙️ Full Options Reference

const ctrl = new CrudController(Model, 'endpoint', {
  // HTTP methods to enable (default: all five)
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],

  // Global middleware for all routes
  middleware: [authMiddleware, loggerMiddleware],

  // Per-operation middleware
  routeMiddleware: {
    create: [validateBody],
    read:   [],
    update: [validateBody],
    delete: [requireAdmin],
  },

  // Custom success/error response shapes
  onSuccess: (res, method, result, meta) => {
    res.status(200).json({ success: true, data: result, ...(meta && { pagination: meta }) });
  },
  onError: (res, method, error) => {
    res.status(500).json({ success: false, error: error.message });
  },

  // Lifecycle hooks
  hooks: {
    beforeCreate: async (req, data) => ({ ...data, createdBy: req.user.id }),
    afterCreate:  async (req, result) => { await notifySlack(result); },
    beforeUpdate: async (req, id, data) => data,
    afterUpdate:  async (req, result) => {},
    beforeDelete: async (req, id) => {},
    afterDelete:  async (req, result) => { await auditLog('delete', result._id); },
    beforeRead:   async (req, query) => ({ ...query, org: req.user.orgId }),
    afterRead:    async (req, result) => result,
  },

  // Validation hooks (run before Mongoose validation)
  validate: {
    create: (data) => ({
      valid: !!data.email && !!data.name,
      errors: [
        ...(!data.email ? ['Email is required'] : []),
        ...(!data.name ? ['Name is required'] : []),
      ],
    }),
    update: (data) => ({ valid: true }),
  },

  // Field selection (Mongoose select syntax)
  select: 'name email role -_id',

  // Auto-populate references
  populate: 'department',
  // or: populate: [{ path: 'department', select: 'name' }],

  // Search fields for GET /endpoint/search
  searchFields: ['name', 'email'],

  // Soft delete (sets deletedAt instead of removing)
  softDelete: true,

  // Aggregation pipeline (static or dynamic)
  aggregatePipeline: [
    { $match: { status: 'Active' } },
    { $sort: { createdAt: -1 } },
  ],
  // or dynamic:
  // aggregatePipeline: (req) => [{ $match: { region: req.query.region } }],

  // Related model cascading
  relatedModel: ProfileModel,
  relatedField: 'userId',
  relatedMethods: ['POST', 'DELETE'],

  // Custom routes (always registered regardless of methods filter)
  customRoutes: [
    {
      method: 'get',
      path: '/stats',
      middleware: [cacheMiddleware],
      handler: async (req, res) => {
        const count = await Model.countDocuments({ status: 'Active' });
        res.json({ activeUsers: count });
      },
    },
  ],
});

📡 Query Parameters

GET All — GET /api/users?...

| Param | Example | Description | |-------|---------|-------------| | filter | {"status":"Active"} | MongoDB filter object | | sort | {"createdAt":-1} | Sort order | | page | 1 | Page number (default: 1) | | limit | 10 | Results per page (default: 10) | | select | name,email | Fields to include/exclude | | populate | author,comments | References to populate | | includeDeleted | true | Include soft-deleted records |

Search — GET /api/users/search?...

| Param | Example | Description | |-------|---------|-------------| | q | john | Search term (required) | | fields | name,email | Override default searchFields | | page | 1 | Page number | | limit | 10 | Results per page | | select | name,email | Fields to include | | populate | author | References to populate |

Pagination Response Shape

{
  "data": [...],
  "pagination": {
    "total": 150,
    "page": 2,
    "limit": 10,
    "pages": 15,
    "hasNext": true,
    "hasPrev": true
  }
}

🪝 Lifecycle Hooks

Hooks let you inject business logic without fighting the abstraction:

hooks: {
  // Transform data before saving — return the modified object
  beforeCreate: async (req, data) => {
    data.createdBy = req.user.id;
    data.slug = slugify(data.name);
    return data;
  },

  // Side-effects after saving
  afterCreate: async (req, result) => {
    await sendWelcomeEmail(result.email);
    await auditLog('user.created', result._id);
  },

  // Scope all reads to the user's organization
  beforeRead: async (req, query) => {
    return { ...query, organizationId: req.user.orgId };
  },

  // Prevent deletion of system records
  beforeDelete: async (req, id) => {
    const item = await User.findById(id);
    if (item?.role === 'system') {
      throw new Error('Cannot delete system users');
    }
  },
}

🗑️ Soft Delete

Enable soft delete to preserve data while hiding it from default queries:

const ctrl = new CrudController(User, 'users', {
  softDelete: true,
});
  • DELETE /users/:id → Sets deletedAt: Date instead of removing
  • GET /users → Auto-excludes records with deletedAt
  • GET /users?includeDeleted=true → Shows everything including deleted
  • PATCH /users/:id/restore → Removes deletedAt to restore the record

📦 Bulk Operations

# Bulk Create
POST /api/users/bulk
Body: [{ "name": "Alice" }, { "name": "Bob" }]

# Bulk Update (by filter)
PATCH /api/users/bulk
Body: { "filter": { "role": "user" }, "update": { "status": "inactive" } }

# Bulk Delete (by IDs)
DELETE /api/users/bulk
Body: { "ids": ["id1", "id2", "id3"] }

🔒 Per-Route Middleware

Apply different middleware to different operations:

const ctrl = new CrudController(User, 'users', {
  middleware: [loggerMiddleware],  // applies to ALL routes
  routeMiddleware: {
    create: [requireAuth, validateBody],
    read:   [optionalAuth],
    update: [requireAuth, requireOwner],
    delete: [requireAuth, requireAdmin],
  },
});

💡 Multiple Controllers

Mount multiple controllers on the same app:

const userCtrl    = new CrudController(User, 'users', { ... });
const productCtrl = new CrudController(Product, 'products', { ... });
const orderCtrl   = new CrudController(Order, 'orders', { ... });

app.use('/api', userCtrl.getRouter());
app.use('/api', productCtrl.getRouter());
app.use('/api', orderCtrl.getRouter());

📋 API Methods

| Method | Returns | Description | |--------|---------|-------------| | getRouter() | Router | Express Router with all configured routes | | getRoutes() | RouteInfo[] | Array of registered route definitions |


🔄 Migration from v1.x

Breaking Changes

  1. onSuccess signature — Now receives an optional 4th meta parameter for pagination metadata
  2. Custom routes — No longer gated by the methods filter; they always register
  3. Soft delete — When softDelete: true, DELETE behavior changes from removing to marking
  4. Bulk delete safetyDELETE /endpoint now requires a non-empty filter to prevent accidental full-table deletes

New Defaults

  • Methods array now includes 'PATCH' by default
  • GET all returns pagination metadata in the default response shape

Upgrade Steps

  1. Update your package: npm install crud-api-express@latest
  2. If your onSuccess callback has strict arity checks, add the optional meta parameter
  3. Test your custom routes — they will now register even if their HTTP method isn't in the methods array
  4. If using deletion endpoints, ensure you pass filters for bulk delete

🔧 CommonJS Usage

const CrudController = require('crud-api-express');
const User = require('./models/User');

const ctrl = new CrudController(User, 'users', { ... });

📖 TypeScript Support

All types are exported for full TypeScript support:

import CrudController, {
  CrudOptions,
  MiddlewareFunction,
  SuccessHandler,
  ErrorHandler,
  ValidationResult,
  PaginationMeta,
  HttpMethod,
  RouteInfo,
} from 'crud-api-express';

License

This project is licensed under the ISC License.

Support Me! ❤️

If you find this package useful, consider supporting me: Buy Me a Coffee ☕