Express.js extension with built-in validation, and type safety
npm install @fishka/expressFunctional utilities for Express.js with type-safe validation and error handling.
``bash`
npm install @fishka/express
This package provides standalone utility functions that work directly with Express. Each feature can be used independently - mix and match what you need.
`typescript
import express from 'express';
import { addErrorHandling, body, patchExpressAsyncErrors, pathParam, queryParam } from '@fishka/express';
import { assertString } from '@fishka/assertions';
const app = express();
app.use(express.json());
// Enable automatic async error catching (call once at startup)
patchExpressAsyncErrors();
// GET /users/:id - with typed path params
app.get('/users/:id', async (req, res) => {
const id = pathParam(req, 'id'); // string - validated inline
res.json({ id, name: 'John' });
});
// GET /users - with query params
app.get('/users', async (req, res) => {
const page = queryParam(req, 'page'); // string - throws 400 if missing
res.json({ page, users: [] });
});
// POST /users - with body validation
app.post('/users', async (req, res) => {
const data = body(req, { name: assertString });
res.json({ id: 1, name: data.name }); // validators infer a type for the data!
});
// Add error handling middleware (must be last)
addErrorHandling(app);
app.listen(3000);
`
Get and validate a path parameter from Express request.
`typescript
import { pathParam, transform, toInt, minLength } from '@fishka/express';
app.get('/users/:id', async (req, res) => {
// Simple - returns string, throws 400 if missing
const id1 = pathParam(req, 'id');
// With validation - transform to number
const id2 = pathParam(req, 'id', transform(toInt()));
// Transform with validation - string min length
const slug = pathParam(req, 'slug', transform(minLength(3)));
res.json({ id: '...' });
});
`
Get and validate a query parameter from Express request.
`typescript
import { queryParam, transform, toInt, min, range, oneOf } from '@fishka/express';
app.get('/users', async (req, res) => {
// Simple - returns string, throws 400 if missing
const search = queryParam(req, 'search');
// With validation - transform to number with min value
const page = queryParam(req, 'page', transform(toInt(), min(1)));
// With validation - number range
const limit = queryParam(req, 'limit', transform(toInt(), range(1, 100)));
// With validation - enum
const sort = queryParam(req, 'sort', transform(oneOf('asc', 'desc')));
res.json({ page, limit, users: [] });
});
`
Get and validate the request body.
`typescript
import { body } from '@fishka/express';
import { assertString, assertNumber } from '@fishka/assertions';
app.post('/users', async (req, res) => {
// Object assertion - validates and infers a type.
const data = body(req, {
name: assertString,
age: assertNumber,
});
res.json({ id: 1, name: data.name });
});
`
Patches Express to automatically catch async errors. Call once at application startup.
`typescript
import { patchExpressAsyncErrors } from '@fishka/express';
// Call before defining routes
patchExpressAsyncErrors();
// Now async route handlers don't need try/catch
app.get('/users', async (req, res) => {
const users = await db.users.findAll(); // Errors are caught automatically
res.json(users);
});
`
Adds centralized error handling middleware. Must be called after all routes.
`typescript
import { addErrorHandling, HttpError } from '@fishka/express';
// Define routes...
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
assertHttp(user, 404, 'User not found');
res.json(user);
});
// Add error handling last
addErrorHandling(app);
`
Use transform() to validate and transform parameters. All operators are composable:
`typescript
import { transform, toInt, minLength, matches, min, range, oneOf } from '@fishka/express';
app.get('/users/:id', async (req, res) => {
const id = pathParam(req, 'id', transform(toInt())); // string → number
const page = queryParam(req, 'page', transform(toInt(), min(1))); // number >= 1
const limit = queryParam(req, 'limit', transform(toInt(), range(1, 100))); // number 1-100
const sort = queryParam(req, 'sort', transform(oneOf('asc', 'desc'))); // enum
const search = queryParam(req, 'search', transform(minLength(3))); // string min 3 chars
res.json({ id, page, limit });
});
`
Transformations (string → T):
- toInt() - parse to integertoNumber()
- - parse to numbertoBool()
- - parse 'true'/'false' to booleanoneOf('a', 'b')
- - enum values
String validators:
- minLength(n) - minimum lengthmaxLength(n)
- - maximum lengthmatches(/regex/)
- - regex matchtrim
- - trim whitespacelowercase
- / uppercase - case transform
Number validators:
- min(n) - minimum valuemax(n)
- - maximum valuerange(min, max)
- - value range
Generic:
- transform(...ops) - chain of validators/transformerscheck(predicate, msg)
- - custom validation with predicatevalidator(fn)
- - custom validator returning string|undefinedmap(fn)
- - transform value
- pathParam(req, 'name') - returns string (throws 400 if missing)queryParam(req, 'name')
- - returns string (throws 400 if missing/empty)queryParam(req, 'name', validator)
- - returns validated value (throws 400 if missing/empty/invalid)
- All parameters are required - missing or empty values throw BAD_REQUEST
`typescript
import { createAuthMiddleware, BasicAuthStrategy, getAuthUser } from '@fishka/express';
const auth = new BasicAuthStrategy(async (user, pass) =>
user === 'admin' && pass === 'secret' ? { id: '1', role: 'admin' } : null,
);
app.get('/profile', createAuthMiddleware(auth), async (req, res) => {
const user = getAuthUser(req);
res.json({ id: user.id });
});
`
`typescript
import { createRateLimiterMiddleware } from '@fishka/express';
app.use(
await createRateLimiterMiddleware({
points: { read: 100, write: 50 },
duration: 60,
}),
);
`
Use HttpError for specific status codes:
`typescript
import { HttpError, HTTP_UNAUTHORIZED, HTTP_NOT_FOUND } from '@fishka/express';
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!req.headers.authorization) {
throw new HttpError(HTTP_UNAUTHORIZED, 'Authorization required');
}
if (!user) {
throw new HttpError(HTTP_NOT_FOUND, 'User not found');
}
res.json(user);
});
`
Use assertHttp for inline assertions:
`typescript
import { assertHttp, HTTP_UNAUTHORIZED, HTTP_NOT_FOUND } from '@fishka/express';
app.get('/admin', async (req, res) => {
assertHttp(req.headers.authorization, HTTP_UNAUTHORIZED, 'Authorization required');
const user = await db.users.findById(req.params.id);
assertHttp(user, HTTP_NOT_FOUND, 'User not found');
assertHttp(user.isAdmin, HTTP_FORBIDDEN, 'Admin access required');
res.json({ admin: true });
});
`
`typescript
import { configureExpressApi, createTlsMiddleware } from '@fishka/express';
// Enable request ID with custom header name
configureExpressApi({
requestIdHeader: 'x-request-id', // or 'x-correlation-id', 'trace-id', etc.
trustRequestIdHeader: true, // default: true, if we trust and propagate request ID passed by client.
});
// Add TLS middleware to track request context
app.use(createTlsMiddleware());
`
Handle uncaught errors and graceful shutdown:
`typescript
import { installProcessHandlers } from '@fishka/express';
installProcessHandlers({
onUncaughtException: err => sendToMonitoring(err),
onUnhandledRejection: reason => sendToMonitoring(reason),
onShutdown: async () => {
await database.close();
await server.close();
},
shutdownTimeout: 15000,
});
`
`typescript
import express from 'express';
import {
addErrorHandling,
body,
min,
patchExpressAsyncErrors,
pathParam,
queryParam,
range,
toInt,
transform,
} from '@fishka/express';
import { assertString } from '@fishka/assertions';
import { assertHttp } from './api.types';
const app = express();
app.use(express.json());
// Enable automatic async error catching
patchExpressAsyncErrors();
// Routes
app.get('/users/:id', async (req, res) => {
const id = pathParam(req, 'id', transform(toInt()));
const user = await db.users.findById(id);
assertHttp(user, 404, 'User not found');
res.json(user);
});
app.get('/users', async (req, res) => {
const page = queryParam(req, 'page', transform(toInt(), min(1)));
const limit = queryParam(req, 'limit', transform(toInt(), range(1, 100)));
const users = await db.users.findAll({ page, limit });
res.json({ users, page, limit });
});
app.post('/users', async (req, res) => {
const data = body(req, { name: assertString });
const user = await db.users.create(data);
res.status(201).json(user);
});
// Error handling (must be last)
addErrorHandling(app);
app.listen(3000);
``
MIT