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.
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());