# 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)
