UNPKG

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
# crud-api-express ![npm](https://img.shields.io/npm/v/crud-api-express) ![downloads](https://img.shields.io/npm/dm/crud-api-express) ![license](https://img.shields.io/npm/l/crud-api-express) ![made-with-node](https://img.shields.io/badge/Made%20with-Node.js-green) ![made-with-typescript](https://img.shields.io/badge/Made%20with-TypeScript-blue) ![express](https://img.shields.io/badge/Express.js-000000?style=flat&logo=express&logoColor=white) ![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=flat&logo=mongodb&logoColor=white) > 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)