Express Controller Sets

A unified toolkit for Express.js that provides pre-built CRUD logic, seamless Multer integration, and robust AWS S3 file upload handling.

Node 20+ Mongoose 9+ Express 5+ Multer 2+

Designed to help you build production-ready APIs faster by automating repetitive controller logic and complex S3 middleware configuration while maintaining type safety and maximum flexibility.

Installation

Install the package using your favorite package manager:

npm install express-controller-sets mongoose express multer multer-s3 dotenv @aws-sdk/client-s3

Environment Variables

Configure your S3 credentials via environment variables:

S3_ENDPOINT=your-endpoint.com
S3_SPACES_KEY=your-key
S3_SPACES_SECRET=your-secret
S3_BUCKET_NAME=your-bucket
S3_REGION=us-east-1

Global Error Handling

To ensure all errors (including database validation failures) are returned in professional JSON format, use the built-in errorHandler middleware.

import express from 'express';
import { createRouter, errorHandler } from 'express-controller-sets';

const app = express();
app.use(express.json());

// ... your routes ...

// MUST BE ADDED LAST: Global JSON Error Handler
app.use(errorHandler);

Standardized Error Response

All errors caught by this middleware will return a consistent 400 or 500 status code with the following body:

{
    "success": false,
    "error": "Detailed error message here",
    "stack": "... (only visible in development) ..."
}

Quick Start

Build a full-featured API for your model in just a few lines of code.

import express from 'express';
import { createRouter } from 'express-controller-sets';
import Product from './models/Product.js';

const app = express();
app.use(express.json());

// Create all CRUD routes automatically
const productRouter = createRouter({
    model: Product,
    orderBy: '-createdAt', // Sort by newest
    search: ['name', 'category.name'], // Enable ?search= or ?s= for multi-field search (including relational)
    query: ['category']    // Enable ?category= filtering
});

app.use('/api/products', productRouter);

Controller Sets

The ControllerSets class provides the core logic for handling Mongoose operations. You can use it directly for custom route handling.

import { ControllerSets } from 'express-controller-sets';
import Product from '../models/Product.js';

const productController = new ControllerSets(
    Product,
    '-createdAt',        // Default sort
    ['category'],        // Filterable fields
    ['name', 'tags'],    // Searchable fields array
    async (doc) => {}    // Optional runAfterCreate callback
);

// Use in manual routes
router.get('/', productController.getAll);
router.post('/', productController.create);

S3 Upload Middleware

Handle file uploads to S3-compatible storage with ease. The middleware automatically updates req.body with S3 locations.

import { fileUploadMiddleware } from 'express-controller-sets';

router.post('/upload', (req, res, next) => {
    fileUploadMiddleware(req, res, next, 'uploads/', [
        { name: 'avatar', maxCount: 1 }
    ]);
}, (req, res) => {
    res.json({ url: req.body.avatar });
});

Image Compression & Optimization

You can optimize and compress uploaded images automatically using the imgOptimizations feature. It leverages Sharp under the hood to perform efficient, multi-pass binary search quality optimization, reducing file size while keeping resolutions intact.

Supported levels:

  • low: Compress image to target 75% - 80% of original file size.
  • medium / med: Compress image to target 60% - 65% of original file size.
  • high: Compress image to target 40% - 45% of original file size.

There are two ways to apply image optimizations:

1. Route-Level Setup (Static Default)

Specify the default compression level when initializing your router:

import { createRouterS3upload } from 'express-controller-sets';

const router = createRouterS3upload({
    model: User,
    path: 'avatars/',
    fields: [{ name: 'avatar', maxCount: 1 }],
    imgOptimizations: 'medium' // All avatars will be compressed to 60-65% size by default
});

2. Client-Controlled (Dynamic Override)

Clients can dynamically specify or override the optimization level per-request by passing the imgOptimizations parameter. This parameter can be passed in the query string, multipart form-data fields, or headers (imgOptimizations or x-img-optimizations):

// Dynamic query parameter override
fetch('/api/users?imgOptimizations=high', {
    method: 'POST',
    body: formData
});

Dynamic Routers

Use the helper functions to quickly scaffold entire resource routers with pre-configured CRUD and upload logic.

Basic Router

import { createRouter } from 'express-controller-sets';
import Product from './models/Product.js';

const router = createRouter({
    model: Product,
    orderBy: '-createdAt',
    search: ['name', 'description']
});

Router with S3 Upload

Automatically combines CRUD logic with S3 file upload middleware.

import { createRouterS3upload } from 'express-controller-sets';
import User from './models/User.js';

const router = createRouterS3upload({
    model: User,
    folder: 'avatars/',
    filesState: [{ name: 'avatar', maxCount: 1 }]
});

TypeScript Support

The package comes with built-in TypeScript definitions. You can pass your Mongoose document interfaces to strongly type the controllers and routers.

import express from 'express';
import { createRouter } from 'express-controller-sets';
import mongoose, { Document } from 'mongoose';

// 1. Define your interface
interface IUser extends Document {
    name: string;
    email: string;
    age: number;
}

// 2. Define or import your model
const UserModel = mongoose.model<IUser>('User', new mongoose.Schema({
    name: String,
    email: String,
    age: Number
}));

// 3. Create a strongly-typed router
const userRouter = createRouter<IUser>({
    model: UserModel,
    orderBy: 'name',
    search: ['name', 'email']
});

const app = express();
app.use('/api/users', userRouter);

API Reference

Option Type Description
model Mongoose Model Required model for database operations.
orderBy String Default sorting field (e.g. '-createdAt').
query Array<String> Fields allowed for automatic query filtering.
search Array<String> | String Array of field names (or a single string) for regex searching. Supports relation dot notation (e.g. category.name).
runAfterCreate Function Optional callback executed after successful create.
middlewares Array<Function> Array of middlewares applied to all routes.
imgOptimizations String Optional default image optimization preset for S3 uploads: 'low', 'medium', 'med', or 'high'.

Note on Pagination

Pagination is supported out-of-the-box for getAll queries. By appending ?page=1&pageSize=50, the response automatically provides a pagination metadata object alongside your results.

API Endpoints

All routers created with createRouter or createRouterS3upload follow standard RESTful patterns and include a powerful built-in filtering engine.

Method Endpoint Description
GET / List records with support for pagination, search, and custom filters.
POST / Create a new record. Supports multipart file uploads on S3 routers.
GET /:id Retrieve a single record by its 24-character hex ID.
PATCH /:id Update an existing record partially. Supports file updates on S3 routers.
DELETE /:id Permanently remove a record from the database.

Query Parameters (Filtering Engine)

The GET / endpoint supports several parameters out-of-the-box to filter and paginate your data.

Parameter Description Example
page The page number (starts at 1). ?page=2
pageSize Number of items per page (default: 10, max: 100). ?pageSize=50
search / s Global search across configured fields. ?search=laptop
sort Field to sort by. Prefix with - for descending. ?sort=-price
[fieldName] Direct equality filter for any configured query field. ?category=65a...
rangeField The numeric field name to apply a range filter on. Works together with range parameter. ?rangeField=price
range The range boundaries in format min-max. Supports min-only (min-) or max-only (-max) ranges. ?range=10-100
compareField The field name to perform a dynamic comparison on (e.g., price, age, etc.). ?compareField=price
compareValue The target value to compare against. Tries to parse as a number if valid, otherwise treats as a string. ?compareValue=50
compareOperator The comparison operator: gt (>), gte (>=), lt (<), lte (<=), ne (!=), eq (==). Default is eq. ?compareOperator=gt

Usage Examples

1. Fetching Paginated & Sorted Results

// Fetch the second page of active products, sorted by price
fetch('/api/products?page=2&pageSize=20&sort=-price&status=active')
    .then(res => res.json())
    .then(data => {
        console.log(data.data); // Array of items
        console.log(data.pagination); // { total, page, pageSize, totalPage }
    });

2. Filtering by Numeric Range

Filter records dynamically by a numeric range using rangeField and range query parameters.

// Fetch products priced between $10 and $100
fetch('/api/products?rangeField=price&range=10-100')
    .then(res => res.json())
    .then(data => console.log(data.data));

// Fetch users aged 18 or older (min only)
fetch('/api/users?rangeField=age&range=18-')
    .then(res => res.json())
    .then(data => console.log(data.data));

3. Dynamic Field Comparison

Filter records dynamically on a field using comparison operators like greater than (gt), greater than or equal (gte), less than (lt), less than or equal (lte), not equal (ne), or equal (eq).

// Fetch products with price greater than 50
fetch('/api/products?compareField=price&compareValue=50&compareOperator=gt')
    .then(res => res.json())
    .then(data => console.log(data.data));

// Fetch active users with age less than or equal to 30
fetch('/api/users?status=active&compareField=age&compareValue=30&compareOperator=lte')
    .then(res => res.json())
    .then(data => console.log(data.data));

4. Creating a Record with File Upload

When using createRouterS3upload, ensure you use FormData for multipart requests.

const formData = new FormData();
formData.append('name', 'New User');
formData.append('avatar', fileInput.files[0]);

fetch('/api/users', {
    method: 'POST',
    body: formData
    // Note: Don't set Content-Type header manually, 
    // fetch will set it for multipart/form-data with boundary automatically.
}).then(res => res.json());