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
Maintainers
Readme
crud-api-express
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 hooks —
beforeCreate,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
$setsemantics - 🔗 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→ SetsdeletedAt: Dateinstead of removingGET /users→ Auto-excludes records withdeletedAtGET /users?includeDeleted=true→ Shows everything including deletedPATCH /users/:id/restore→ RemovesdeletedAtto 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
onSuccesssignature — Now receives an optional 4thmetaparameter for pagination metadata- Custom routes — No longer gated by the
methodsfilter; they always register - Soft delete — When
softDelete: true, DELETE behavior changes from removing to marking - Bulk delete safety —
DELETE /endpointnow 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
- Update your package:
npm install crud-api-express@latest - If your
onSuccesscallback has strict arity checks, add the optionalmetaparameter - Test your custom routes — they will now register even if their HTTP method isn't in the
methodsarray - 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 ☕
