Skip to content

Repository files navigation

Express API

Part of fishka.bio — free browser-based bioinformatics tools.

Functional utilities for Express.js with type-safe validation and error handling.

Installation

npm install @fishka/express

Overview

This package provides standalone utility functions that work directly with Express. Each feature can be used independently - mix and match what you need.

Quick Start

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

Utilities

pathParam(req, name, validator?)

Get and validate a path parameter from Express request.

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: '...' });
});

queryParam(req, name, validator?)

Get and validate a query parameter from Express request.

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: [] });
});

body(req, validator)

Get and validate the request body.

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 });
});

patchExpressAsyncErrors()

Patches Express to automatically catch async errors. Call once at application startup.

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);
});

addErrorHandling(app)

Adds centralized error handling middleware. Must be called after all routes.

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

Parameter Validation

Use transform() to validate and transform parameters. All operators are composable:

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 });
});

Available Operators

Transformations (string → T):

  • toInt() - parse to integer
  • toNumber() - parse to number
  • toBool() - parse 'true'/'false' to boolean
  • oneOf('a', 'b') - enum values

String validators:

  • minLength(n) - minimum length
  • maxLength(n) - maximum length
  • matches(/regex/) - regex match
  • trim - trim whitespace
  • lowercase / uppercase - case transform

Number validators:

  • min(n) - minimum value
  • max(n) - maximum value
  • range(min, max) - value range

Generic:

  • transform(...ops) - chain of validators/transformers
  • check(predicate, msg) - custom validation with predicate
  • validator(fn) - custom validator returning string|undefined
  • map(fn) - transform value

Parameter Requirements

  • 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

Authentication

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 });
});

Rate Limiting

import { createRateLimiterMiddleware } from '@fishka/express';

app.use(
  await createRateLimiterMiddleware({
    points: { read: 100, write: 50 },
    duration: 60,
  }),
);

HTTP Errors

Use HttpError for specific status codes:

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:

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 });
});

Configuration

Request ID

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

Process Handlers

Handle uncaught errors and graceful shutdown:

import { installProcessHandlers } from '@fishka/express';

installProcessHandlers({
  onUncaughtException: err => sendToMonitoring(err),
  onUnhandledRejection: reason => sendToMonitoring(reason),
  onShutdown: async () => {
    await database.close();
    await server.close();
  },
  shutdownTimeout: 15000,
});

Complete Example

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

License

MIT

About

Express.js extension with built-in validation, and type safety

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages