crud-api-express
Version:
A powerful, flexible CRUD controller for Express + Mongoose — auto-generates RESTful endpoints with lifecycle hooks, validation, soft delete, search, bulk operations, pagination, and more.
410 lines (316 loc) • 12.3 kB
Markdown
# 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 `$set` semantics
- 🔗 **Related model cascading** — auto‑create/update/delete linked models
- 📝 **Full TypeScript support** — exported types, generics, JSDoc
## 📦 Installation
```bash
npm install crud-api-express
# Peer dependencies (install alongside):
npm install express mongoose
```
## 🚀 Quick Start
```javascript
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
```typescript
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
```json
{
"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:
```javascript
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:
```javascript
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
```bash
# 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:
```javascript
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:
```javascript
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 safety** — `DELETE /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
```javascript
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:
```typescript
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 ☕](https://buymeacoffee.com/mrider007)