crud-api-express 1.2.6 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # CRUD API Controller
1
+ # crud-api-express
2
2
 
3
3
  ![npm](https://img.shields.io/npm/v/crud-api-express)
4
4
  ![downloads](https://img.shields.io/npm/dm/crud-api-express)
@@ -8,271 +8,396 @@
8
8
  ![express](https://img.shields.io/badge/Express.js-000000?style=flat&logo=express&logoColor=white)
9
9
  ![MongoDB](https://img.shields.io/badge/MongoDB-47A248?style=flat&logo=mongodb&logoColor=white)
10
10
 
11
+ > 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.
11
12
 
13
+ ---
14
+
15
+ ## ✨ Features
16
+
17
+ - 🚀 **Zero boilerplate** — full CRUD in 3 lines of code
18
+ - 🪝 **Lifecycle hooks** — `beforeCreate`, `afterUpdate`, `beforeDelete`, etc.
19
+ - ✅ **Validation hooks** — reject bad data before it hits Mongoose
20
+ - 🔍 **Search endpoint** — case-insensitive text search across multiple fields
21
+ - 🗑️ **Soft delete** — mark records as deleted + restore endpoint
22
+ - 📦 **Bulk operations** — create, update, and delete in batch
23
+ - 🔒 **Per-route middleware** — different auth/logic for read vs. write
24
+ - 📄 **Pagination metadata** — total, pages, hasNext, hasPrev
25
+ - 🎯 **Field selection & population** — `?select=name,email&populate=author`
26
+ - 🔢 **Count & exists** — lightweight endpoints for checking data
27
+ - 📊 **Dynamic aggregation** — static pipelines or functions of `req`
28
+ - 🏗️ **PATCH support** — partial updates with `$set` semantics
29
+ - 🔗 **Related model cascading** — auto‑create/update/delete linked models
30
+ - 📝 **Full TypeScript support** — exported types, generics, JSDoc
12
31
 
13
- ## Installation
32
+ ---
14
33
 
15
- Install the package using npm:
34
+ ## 📦 Installation
16
35
 
17
36
  ```bash
18
37
  npm install crud-api-express
38
+ # Peer dependencies (install alongside):
39
+ npm install express mongoose
19
40
  ```
20
- This project provides a flexible and reusable CRUD (Create, Read, Update, Delete) API controller for MongoDB using Express.js and Mongoose.
21
41
 
22
- ## Docs! ❤️
23
- [Doc Page Visit here](https://mukeshdev.vercel.app/crudapi)
42
+ ---
24
43
 
25
- ## 📌 Table of Contents
44
+ ## 🚀 Quick Start
45
+
46
+ ```javascript
47
+ import express from 'express';
48
+ import mongoose from 'mongoose';
49
+ import CrudController from 'crud-api-express';
26
50
 
27
- - [Introduction](#introduction)
28
- - [Installation](#installation)
29
- - [Usage](#usage)
30
- - [API](#api)
31
- - [Options](#options)
32
- - [License](#license)
51
+ // 1. Define your model
52
+ const UserSchema = new mongoose.Schema({
53
+ name: { type: String, required: true },
54
+ email: { type: String, required: true, unique: true },
55
+ role: { type: String, default: 'user', enum: ['user', 'admin'] },
56
+ }, { timestamps: true, versionKey: false });
33
57
 
58
+ const User = mongoose.model('User', UserSchema);
34
59
 
60
+ // 2. Create the controller
61
+ const userCtrl = new CrudController(User, 'users');
35
62
 
36
- ---
63
+ // 3. Mount and go
64
+ const app = express();
65
+ app.use(express.json());
66
+ app.use('/api', userCtrl.getRouter());
37
67
 
38
- ## Introduction
68
+ mongoose.connect('mongodb://localhost:27017/mydb').then(() => {
69
+ app.listen(3000, () => console.log('Server running on port 3000'));
70
+ });
71
+ ```
39
72
 
40
- The `CrudController` class simplifies the creation of RESTful APIs in Node.js applications using MongoDB. It abstracts away common CRUD operations, error handling, middleware integration, and supports custom routes and aggregation pipelines.
73
+ That's it you now have **15+ endpoints** auto-generated. 🎉
41
74
 
42
75
  ---
43
76
 
44
- ## Usage
77
+ ## 🛣️ Auto-Generated Endpoints
78
+
79
+ | Method | Endpoint | Description |
80
+ |--------|----------|-------------|
81
+ | `POST` | `/users` | Create a record |
82
+ | `POST` | `/users/bulk` | Bulk create records |
83
+ | `GET` | `/users` | List all (filter/sort/paginate/select/populate) |
84
+ | `GET` | `/users/:id` | Get one by ID |
85
+ | `GET` | `/users/search` | Text search across fields |
86
+ | `GET` | `/users/count` | Count matching records |
87
+ | `GET` | `/users/exists/:id` | Check if a record exists |
88
+ | `GET` | `/users/aggregate` | Run aggregation pipeline |
89
+ | `PUT` | `/users/:id` | Full update by ID |
90
+ | `PATCH` | `/users/:id` | Partial update by ID |
91
+ | `PATCH` | `/users/bulk` | Bulk update by filter |
92
+ | `PATCH` | `/users/:id/restore` | Restore soft-deleted record *(soft delete only)* |
93
+ | `DELETE` | `/users/:id` | Delete one by ID |
94
+ | `DELETE` | `/users` | Delete by filter |
95
+ | `DELETE` | `/users/bulk` | Bulk delete by IDs array |
45
96
 
46
- Here's a basic example of how to use in Es module `CrudController`:
97
+ ---
47
98
 
48
- ```javascript
49
- import express from 'express';
50
- import mongoose from 'mongoose';
51
- import CrudController from 'crud-api-express';
99
+ ## ⚙️ Full Options Reference
100
+
101
+ ```typescript
102
+ const ctrl = new CrudController(Model, 'endpoint', {
103
+ // HTTP methods to enable (default: all five)
104
+ methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
105
+
106
+ // Global middleware for all routes
107
+ middleware: [authMiddleware, loggerMiddleware],
52
108
 
53
- const Schema = mongoose.Schema;
54
- const ExampleSchema = new Schema(
55
- {
56
- type: { type: String, default: 'Percentage', enum: ['Percentage', 'Flat'] },
57
- status: { type: String, default: 'Active', trim: true },
58
- expiry_date: { type: Date, index: true, trim: true },
109
+ // Per-operation middleware
110
+ routeMiddleware: {
111
+ create: [validateBody],
112
+ read: [],
113
+ update: [validateBody],
114
+ delete: [requireAdmin],
59
115
  },
60
- { timestamps: true, versionKey: false }
61
- );
62
-
63
- const ExampleModel = mongoose.model('Example', ExampleSchema);
64
-
65
- const options = {
66
- middleware: [
67
- (req, res, next) => {
68
- const authToken = req.headers.authorization;
69
- if (!authToken) {
70
- return res.status(401).json({ message: 'Unauthorized' });
71
- }
72
- next();
73
- },
74
- (req, res, next) => {
75
- console.log(`Request received at ${new Date()}`);
76
- next();
77
- },
78
- ],
79
- onSuccess: (res, method, result) => {
80
- console.log(`Successful ${method} operation:`, result);
81
- res.status(200).json({ success: true, data: result });
116
+
117
+ // Custom success/error response shapes
118
+ onSuccess: (res, method, result, meta) => {
119
+ res.status(200).json({ success: true, data: result, ...(meta && { pagination: meta }) });
82
120
  },
83
121
  onError: (res, method, error) => {
84
- console.error(`Error in ${method} operation:`, error);
85
- res.status(500).json({ error: error.message });
122
+ res.status(500).json({ success: false, error: error.message });
123
+ },
124
+
125
+ // Lifecycle hooks
126
+ hooks: {
127
+ beforeCreate: async (req, data) => ({ ...data, createdBy: req.user.id }),
128
+ afterCreate: async (req, result) => { await notifySlack(result); },
129
+ beforeUpdate: async (req, id, data) => data,
130
+ afterUpdate: async (req, result) => {},
131
+ beforeDelete: async (req, id) => {},
132
+ afterDelete: async (req, result) => { await auditLog('delete', result._id); },
133
+ beforeRead: async (req, query) => ({ ...query, org: req.user.orgId }),
134
+ afterRead: async (req, result) => result,
86
135
  },
87
- methods: ['GET', 'POST', 'PUT', 'DELETE'],
136
+
137
+ // Validation hooks (run before Mongoose validation)
138
+ validate: {
139
+ create: (data) => ({
140
+ valid: !!data.email && !!data.name,
141
+ errors: [
142
+ ...(!data.email ? ['Email is required'] : []),
143
+ ...(!data.name ? ['Name is required'] : []),
144
+ ],
145
+ }),
146
+ update: (data) => ({ valid: true }),
147
+ },
148
+
149
+ // Field selection (Mongoose select syntax)
150
+ select: 'name email role -_id',
151
+
152
+ // Auto-populate references
153
+ populate: 'department',
154
+ // or: populate: [{ path: 'department', select: 'name' }],
155
+
156
+ // Search fields for GET /endpoint/search
157
+ searchFields: ['name', 'email'],
158
+
159
+ // Soft delete (sets deletedAt instead of removing)
160
+ softDelete: true,
161
+
162
+ // Aggregation pipeline (static or dynamic)
88
163
  aggregatePipeline: [
89
164
  { $match: { status: 'Active' } },
90
165
  { $sort: { createdAt: -1 } },
91
166
  ],
167
+ // or dynamic:
168
+ // aggregatePipeline: (req) => [{ $match: { region: req.query.region } }],
169
+
170
+ // Related model cascading
171
+ relatedModel: ProfileModel,
172
+ relatedField: 'userId',
173
+ relatedMethods: ['POST', 'DELETE'],
174
+
175
+ // Custom routes (always registered regardless of methods filter)
92
176
  customRoutes: [
93
177
  {
94
178
  method: 'get',
95
- path: '/custom-route',
96
- handler: (req, res) => {
97
- res.json({ message: 'Custom route handler executed' });
98
- },
99
- },
100
- {
101
- method: 'post',
102
- path: '/custom-action',
103
- handler: (req, res) => {
104
- res.json({ message: 'Custom action executed' });
179
+ path: '/stats',
180
+ middleware: [cacheMiddleware],
181
+ handler: async (req, res) => {
182
+ const count = await Model.countDocuments({ status: 'Active' });
183
+ res.json({ activeUsers: count });
105
184
  },
106
185
  },
107
186
  ],
108
- };
187
+ });
188
+ ```
109
189
 
110
- const exampleController = new CrudController(ExampleModel, 'examples', options);
190
+ ---
111
191
 
112
- const mongoURI = 'mongodb://localhost:27017/mydatabase';
192
+ ## 📡 Query Parameters
113
193
 
114
- mongoose
115
- .connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true })
116
- .then(() => {
117
- console.log('Connected to MongoDB');
194
+ ### GET All — `GET /api/users?...`
118
195
 
119
- const app = express();
120
- app.use(express.json());
121
- app.use('/api', exampleController.getRouter());
196
+ | Param | Example | Description |
197
+ |-------|---------|-------------|
198
+ | `filter` | `{"status":"Active"}` | MongoDB filter object |
199
+ | `sort` | `{"createdAt":-1}` | Sort order |
200
+ | `page` | `1` | Page number (default: 1) |
201
+ | `limit` | `10` | Results per page (default: 10) |
202
+ | `select` | `name,email` | Fields to include/exclude |
203
+ | `populate` | `author,comments` | References to populate |
204
+ | `includeDeleted` | `true` | Include soft-deleted records |
122
205
 
123
- console.log(exampleController.getRoutes());
206
+ ### Search — `GET /api/users/search?...`
124
207
 
125
- const PORT = process.env.PORT || 3000;
126
- app.listen(PORT, () => {
127
- console.log(`Server is running on port ${PORT}`);
128
- });
129
- })
130
- .catch((err) => {
131
- console.error('Error connecting to MongoDB:', err.message);
132
- process.exit(1);
133
- });
208
+ | Param | Example | Description |
209
+ |-------|---------|-------------|
210
+ | `q` | `john` | Search term (**required**) |
211
+ | `fields` | `name,email` | Override default searchFields |
212
+ | `page` | `1` | Page number |
213
+ | `limit` | `10` | Results per page |
214
+ | `select` | `name,email` | Fields to include |
215
+ | `populate` | `author` | References to populate |
216
+
217
+ ### Pagination Response Shape
218
+
219
+ ```json
220
+ {
221
+ "data": [...],
222
+ "pagination": {
223
+ "total": 150,
224
+ "page": 2,
225
+ "limit": 10,
226
+ "pages": 15,
227
+ "hasNext": true,
228
+ "hasPrev": true
229
+ }
230
+ }
134
231
  ```
135
- Here's a basic example of how to use in cjs module `CrudController`:
136
- ```javascript
137
232
 
138
- const express = require('express');
139
- const mongoose = require('mongoose');
140
- const CrudController = require('crud-api-express');
233
+ ---
234
+
235
+ ## 🪝 Lifecycle Hooks
141
236
 
142
- const Schema = mongoose.Schema;
143
- const ExampleSchema = new Schema(
144
- {
145
- type: { type: String, default: 'Percentage', enum: ['Percentage', 'Flat'] },
146
- status: { type: String, default: 'Active', trim: true },
147
- expiry_date: { type: Date, index: true, trim: true },
237
+ Hooks let you inject business logic without fighting the abstraction:
238
+
239
+ ```javascript
240
+ hooks: {
241
+ // Transform data before saving return the modified object
242
+ beforeCreate: async (req, data) => {
243
+ data.createdBy = req.user.id;
244
+ data.slug = slugify(data.name);
245
+ return data;
148
246
  },
149
- { timestamps: true, versionKey: false }
150
- );
151
-
152
- const ExampleModel = mongoose.model('Example', ExampleSchema);
153
-
154
- const options = {
155
- middleware: [
156
- (req, res, next) => {
157
- const authToken = req.headers.authorization;
158
- if (!authToken) {
159
- return res.status(401).json({ message: 'Unauthorized' });
160
- }
161
- next();
162
- },
163
- (req, res, next) => {
164
- console.log(`Request received at ${new Date()}`);
165
- next();
166
- },
167
- ],
168
- onSuccess: (res, method, result) => {
169
- console.log(`Successful ${method} operation:`, result);
170
- res.status(200).json({ success: true, data: result });
247
+
248
+ // Side-effects after saving
249
+ afterCreate: async (req, result) => {
250
+ await sendWelcomeEmail(result.email);
251
+ await auditLog('user.created', result._id);
171
252
  },
172
- onError: (res, method, error) => {
173
- console.error(`Error in ${method} operation:`, error);
174
- res.status(500).json({ error: error.message });
253
+
254
+ // Scope all reads to the user's organization
255
+ beforeRead: async (req, query) => {
256
+ return { ...query, organizationId: req.user.orgId };
175
257
  },
176
- methods: ['GET', 'POST', 'PUT', 'DELETE'],
177
- aggregatePipeline: [
178
- { $match: { status: 'Active' } },
179
- { $sort: { createdAt: -1 } },
180
- ],
181
- customRoutes: [
182
- {
183
- method: 'get',
184
- path: '/custom-route',
185
- handler: (req, res) => {
186
- res.json({ message: 'Custom route handler executed' });
187
- },
188
- },
189
- {
190
- method: 'post',
191
- path: '/custom-action',
192
- handler: (req, res) => {
193
- res.json({ message: 'Custom action executed' });
194
- },
195
- },
196
- ],
197
- };
198
258
 
199
- const exampleController = new CrudController(ExampleModel, 'examples', options);
259
+ // Prevent deletion of system records
260
+ beforeDelete: async (req, id) => {
261
+ const item = await User.findById(id);
262
+ if (item?.role === 'system') {
263
+ throw new Error('Cannot delete system users');
264
+ }
265
+ },
266
+ }
267
+ ```
200
268
 
201
- const mongoURI = 'mongodb://localhost:27017/mydatabase';
269
+ ---
270
+
271
+ ## 🗑️ Soft Delete
202
272
 
203
- mongoose
204
- .connect(mongoURI, { useNewUrlParser: true, useUnifiedTopology: true })
205
- .then(() => {
206
- console.log('Connected to MongoDB');
273
+ Enable soft delete to preserve data while hiding it from default queries:
207
274
 
208
- const app = express();
209
- app.use(express.json());
210
- app.use('/api', exampleController.getRouter());
275
+ ```javascript
276
+ const ctrl = new CrudController(User, 'users', {
277
+ softDelete: true,
278
+ });
279
+ ```
280
+
281
+ - `DELETE /users/:id` → Sets `deletedAt: Date` instead of removing
282
+ - `GET /users` → Auto-excludes records with `deletedAt`
283
+ - `GET /users?includeDeleted=true` → Shows everything including deleted
284
+ - `PATCH /users/:id/restore` → Removes `deletedAt` to restore the record
285
+
286
+ ---
211
287
 
212
- console.log(exampleController.getRoutes());
288
+ ## 📦 Bulk Operations
213
289
 
214
- const PORT = process.env.PORT || 3000;
215
- app.listen(PORT, () => {
216
- console.log(`Server is running on port ${PORT}`);
217
- });
218
- })
219
- .catch((err) => {
220
- console.error('Error connecting to MongoDB:', err.message);
221
- process.exit(1);
222
- });
290
+ ```bash
291
+ # Bulk Create
292
+ POST /api/users/bulk
293
+ Body: [{ "name": "Alice" }, { "name": "Bob" }]
294
+
295
+ # Bulk Update (by filter)
296
+ PATCH /api/users/bulk
297
+ Body: { "filter": { "role": "user" }, "update": { "status": "inactive" } }
298
+
299
+ # Bulk Delete (by IDs)
300
+ DELETE /api/users/bulk
301
+ Body: { "ids": ["id1", "id2", "id3"] }
223
302
  ```
224
303
 
225
304
  ---
226
305
 
227
- ## API
306
+ ## 🔒 Per-Route Middleware
228
307
 
229
- ### `getRouter(): Router`
230
- Returns the Express Router instance configured with CRUD routes.
308
+ Apply different middleware to different operations:
231
309
 
232
- ```json
233
- [
234
- { "method": "POST", "path": "/examples", "params": null },
235
- { "method": "GET", "path": "/examples", "params": ["filter", "sort", "page", "limit"] },
236
- { "method": "GET", "path": "/examples/:id", "params": ["id"] },
237
- { "method": "PUT", "path": "/examples/:id", "params": ["id"] },
238
- { "method": "DELETE", "path": "/examples", "params": ["filter"] },
239
- { "method": "DELETE", "path": "/examples/:id", "params": ["id"] },
240
- { "method": "GET", "path": "/examples/aggregate", "params": null },
241
- { "method": "GET", "path": "/examples/custom-route", "params": null },
242
- { "method": "POST", "path": "/examples/custom-action", "params": null }
243
- ]
310
+ ```javascript
311
+ const ctrl = new CrudController(User, 'users', {
312
+ middleware: [loggerMiddleware], // applies to ALL routes
313
+ routeMiddleware: {
314
+ create: [requireAuth, validateBody],
315
+ read: [optionalAuth],
316
+ update: [requireAuth, requireOwner],
317
+ delete: [requireAuth, requireAdmin],
318
+ },
319
+ });
244
320
  ```
245
321
 
246
- ## Options
322
+ ---
323
+
324
+ ## 💡 Multiple Controllers
325
+
326
+ Mount multiple controllers on the same app:
327
+
328
+ ```javascript
329
+ const userCtrl = new CrudController(User, 'users', { ... });
330
+ const productCtrl = new CrudController(Product, 'products', { ... });
331
+ const orderCtrl = new CrudController(Order, 'orders', { ... });
332
+
333
+ app.use('/api', userCtrl.getRouter());
334
+ app.use('/api', productCtrl.getRouter());
335
+ app.use('/api', orderCtrl.getRouter());
336
+ ```
337
+
338
+ ---
339
+
340
+ ## 📋 API Methods
341
+
342
+ | Method | Returns | Description |
343
+ |--------|---------|-------------|
344
+ | `getRouter()` | `Router` | Express Router with all configured routes |
345
+ | `getRoutes()` | `RouteInfo[]` | Array of registered route definitions |
346
+
347
+ ---
247
348
 
349
+ ## 🔄 Migration from v1.x
248
350
 
249
- ### `CrudOptions<T>`
351
+ ### Breaking Changes
250
352
 
251
- | Option | Type | Description |
252
- |--------------------|---------------------------------------------------------------------------------------------------------|------------------------------------------------------|
253
- | `middleware` | `((req: Request, res: Response, next: NextFunction) => void)[]` | Array of middleware functions |
254
- | `onSuccess` | `(res: Response, method: string, result: T \| T[]) => void` | Success handler function |
255
- | `onError` | `(res: Response, method: string, error: Error) => void` | Error handler function |
256
- | `methods` | `('POST' \| 'GET' \| 'PUT' \| 'DELETE')[]` | Array of HTTP methods to support |
257
- | `relatedModel` | `Model<any>` | Related Mongoose model for relational operations |
258
- | `relatedField` | `string` | Field name for related models |
259
- | `relatedMethods` | `('POST' \| 'GET' \| 'PUT' \| 'DELETE')[]` | Methods to apply on related models |
260
- | `aggregatePipeline` | `object[]` | MongoDB aggregation pipeline stages |
261
- | `customRoutes` | `{ method: 'post' \| 'get' \| 'put' \| 'delete', path: string, handler: (req: Request, res: Response) => void }[]` | Array of custom route definitions |
353
+ 1. **`onSuccess` signature** — Now receives an optional 4th `meta` parameter for pagination metadata
354
+ 2. **Custom routes** — No longer gated by the `methods` filter; they always register
355
+ 3. **Soft delete** When `softDelete: true`, DELETE behavior changes from removing to marking
356
+ 4. **Bulk delete safety** — `DELETE /endpoint` now requires a non-empty filter to prevent accidental full-table deletes
357
+
358
+ ### New Defaults
359
+
360
+ - Methods array now includes `'PATCH'` by default
361
+ - GET all returns pagination metadata in the default response shape
362
+
363
+ ### Upgrade Steps
364
+
365
+ 1. Update your package: `npm install crud-api-express@latest`
366
+ 2. If your `onSuccess` callback has strict arity checks, add the optional `meta` parameter
367
+ 3. Test your custom routes — they will now register even if their HTTP method isn't in the `methods` array
368
+ 4. If using deletion endpoints, ensure you pass filters for bulk delete
262
369
 
263
370
  ---
264
371
 
265
- ## 📖 Fetch All Records with Query Params (GET)
372
+ ## 🔧 CommonJS Usage
266
373
 
267
- **🛠️ URL:**
268
- `GET http://localhost:3000/api/examples?filter={"status":"Active"}&sort={"expiry_date":1}&page=1&limit=10`
374
+ ```javascript
375
+ const CrudController = require('crud-api-express');
376
+ const User = require('./models/User');
269
377
 
270
- ### 🔍 Query Params Explanation:
271
- - **`filter`** → Filter results (e.g., `{ "status": "Active" }`).
272
- - **`sort`** → Sort order (e.g., `{ "expiry_date": 1 }` for ascending).
273
- - **`page`** → Pagination (e.g., `page=1`).
274
- - **`limit`** → Number of results per page.
378
+ const ctrl = new CrudController(User, 'users', { ... });
379
+ ```
275
380
 
381
+ ---
382
+
383
+ ## 📖 TypeScript Support
384
+
385
+ All types are exported for full TypeScript support:
386
+
387
+ ```typescript
388
+ import CrudController, {
389
+ CrudOptions,
390
+ MiddlewareFunction,
391
+ SuccessHandler,
392
+ ErrorHandler,
393
+ ValidationResult,
394
+ PaginationMeta,
395
+ HttpMethod,
396
+ RouteInfo,
397
+ } from 'crud-api-express';
398
+ ```
399
+
400
+ ---
276
401
 
277
402
  ## License
278
403
 
@@ -280,6 +405,5 @@ This project is licensed under the **ISC License**.
280
405
 
281
406
  ## Support Me! ❤️
282
407
 
283
- If you find this package useful, consider supporting me:
408
+ If you find this package useful, consider supporting me:
284
409
  [Buy Me a Coffee ☕](https://buymeacoffee.com/mrider007)
285
-