ExisJS

Middleware Pipeline

ExisJS provides a unified middleware pipeline supporting three execution levels:

  1. Global Middlewares (src/http/server.ts or exis.config.ts): Apply to every incoming HTTP request across the server.
  2. Folder-Scoped Middlewares (src/http/**/boundary.ts): Apply to all routes within a directory and its subdirectories.
  3. Route-Scoped Middlewares (route.ts): Apply to specific individual endpoints.

All standard middlewares can be imported directly from exisjs/middleware. Custom typed middlewares can be declared using defineMiddleware<TContext>() from exisjs/router.


Built-in Middlewares Reference

MiddlewareSignatureDescription
corscors(options?)Cross-Origin Resource Sharing handler for allowed origins, methods, and preflight headers.
requestLoggerrequestLogger(options?)Request logger powered by Pino, injecting req.log with automatic timing and child loggers.
requestIdrequestId()Attaches a unique X-Request-Id to requests and responses with distributed tracing headers.
compresscompress()Stream-based HTTP response compression supporting Gzip, Brotli, and Deflate.
rateLimitrateLimit(options?)In-memory rate limiter returning 429 Too Many Requests with standard headers.
idempotentidempotent(options?)Duplicate mutation caching keyed by Idempotency-Key headers.
dedupededupe(options)Merges simultaneous identical in-flight GET requests into a single operation.
backpressurebackpressure(options?)Queues requests during high load and sheds excess traffic with 503 Service Unavailable.
serveStaticserveStatic(root, options?)Serves static assets from disk with ETag / Last-Modified revalidation and Brotli/Gzip pre-compression.
timeouttimeout(ms | options)Terminates requests exceeding the duration limit with 503 Service Unavailable.
fileUploadfileUpload(options?)Streaming multipart/form-data parser supporting disk/memory storage and MIME checks.
fromExpressfromExpress(middleware)Connect/Express bridge adapter running third-party middleware (e.g. Multer) directly.

Where to Apply Middleware

1. Global Middleware (src/http/server.ts)

src/http/server.ts
import { exis } from 'exisjs'import { requestId, requestLogger, compress } from 'exisjs/middleware'
export default exis({  async onStart(app) {    app.use(requestId())    app.use(requestLogger({ level: 'info' }))    app.use(compress())  },})

2. Folder-Scoped Middleware (src/http/**/boundary.ts)

Middlewares defined in a boundary.ts apply to all routes in that folder and child folders:

src/http/admin/boundary.ts
import { defineBoundary } from 'exisjs/router'import { rateLimit } from 'exisjs/middleware'import { authGuard } from '../../common/guards/auth.guard'
const adminLimiter = rateLimit({  windowMs: 60 * 1000,  max: 60,  message: 'Admin rate limit exceeded',})
export default defineBoundary({  middleware: [adminLimiter, authGuard],})

3. Route-Scoped Middleware (src/http/**/route.ts)

Functional Route Paradigm

src/http/reports/route.ts
import { controller, route } from 'exisjs/router'import { dedupe } from 'exisjs/middleware'
export default controller({  generate: route.get('/', {    middleware: [      dedupe({        keyGenerator: (req) => `${req.user?.id || req.ip}:reports`,      }),    ],    async handle({ res }) {      const data = await computeReport()      res.json({ data })    },  }),})

Class-Based (OOP) Controller Paradigm

src/http/reports/controller.ts
import { Controller, Get, Use } from 'exisjs/decorators'import { dedupe } from 'exisjs/middleware'
@Controller('/reports')export default class ReportsController {  @Get('/')  @Use(    dedupe({      keyGenerator: (req) => `${req.user?.id || req.ip}:reports`,    })  )  async generate() {    return { data: await computeReport() }  }}

Type-Safe Middleware Context (defineMiddleware)

When custom middlewares augment req with extra properties (such as user sessions or tenant tokens), use defineMiddleware<TContext>() from exisjs/router. Downstream route handlers will automatically infer the injected properties on req:

1. Declaring a Typed Middleware

src/common/middleware/auth.ts
import { defineMiddleware } from 'exisjs/router'import { UnauthorizedException } from 'exisjs/error'
export interface AuthContext {  user: {    id: string    role: 'admin' | 'member'    email: string  }}
export const requireAuth = defineMiddleware<AuthContext>(async (req, res, next) => {  const token = req.headers['authorization']  if (!token) {    throw new UnauthorizedException('Authentication token missing')  }
  req.user = {    id: 'user_456',    role: 'admin',    email: '[email protected]',  }
  next()})

Middleware may be async. ExisJS awaits the returned promise. If it throws, or rejects with an HttpError (such as UnauthorizedException above), that error becomes the response through the error handler, with the standard error envelope. You don't need to catch it and call next(err) yourself.

2. Using Typed Middleware in Functional Routes

src/http/profile/route.ts
import { controller, route } from 'exisjs/router'import { requireAuth } from '../../common/middleware/auth'
export default controller({  me: route.get('/', {    middleware: [requireAuth],    handle({ req, res }) {      // req.user is strongly typed as AuthContext['user'] without manual casts      res.json({        id: req.user.id,        email: req.user.email,        role: req.user.role,      })    },  }),})

Multipart File Uploads (fileUpload)

ExisJS includes a streaming multipart parser in exisjs/middleware for file uploads:

1. Functional Route Upload Declaration

src/http/media/route.ts
import { controller, route } from 'exisjs/router'import { fileUpload } from 'exisjs/middleware'
export default controller({  uploadAvatar: route.post('/avatar', {    middleware: [      fileUpload.single('avatar', {        maxSize: '5MB',        allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],        destination: 'memory',      }),    ],    async handle({ req, res }) {      const file = req.file      if (!file) {        return res.status(400).json({ error: 'No file uploaded' })      }
      return res.status(201).json({        filename: file.filename,        mimetype: file.mimetype,        size: file.size,      })    },  }),})

2. Class-Based (OOP) Upload Decorator

src/http/media/controller.ts
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'import type { ExisFile } from 'exisjs/middleware'
@Controller('/media')export default class MediaController {  @Post('/avatar')  @UseUpload({    field: 'avatar',    maxSize: '5MB',    allowedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'],    destination: 'memory',  })  async uploadAvatar(@UploadedFile() file: ExisFile, @Body() body: Record<string, string>) {    return {      filename: file.filename,      size: file.size,      title: body.title,    }  }}

Express and Connect Middleware Bridge (fromExpress)

To run legacy Express or Connect middleware (such as multer, passport, or custom third-party packages) directly inside ExisJS, wrap them using fromExpress:

src/http/legacy/route.ts
import { controller, route } from 'exisjs/router'import { fromExpress } from 'exisjs/middleware'import multer from 'multer'
const upload = multer({ storage: multer.memoryStorage() })
export default controller({  upload: route.post(    '/',    fromExpress(upload.single('file')),    ({ req, res }) => {      const file = req.file      res.json({ success: true, originalname: file?.filename })    }  ),})

fromExpress automatically maps parameters, query strings, headers, and attached properties (like req.file or req.body) back onto the ExisRequest instance.


Static File Serving (serveStatic)

Serve static frontend bundles, assets, or downloads directly from disk:

src/http/server.ts
import { exis } from 'exisjs'import { serveStatic } from 'exisjs/middleware'import path from 'node:path'
export default exis({  async onStart(app) {    app.use(      '/public',      serveStatic(path.join(process.cwd(), 'public'), {        maxAge: 31536000, // 1 year cache-control        etag: true,        brotli: true,        gzip: true,      })    )  },})

serveStatic handles MIME detection and serves a pre-compressed .br or .gz file when one exists and the client accepts that encoding (br;q=0 counts as a refusal). Every file gets an ETag (from its size and modification time; each encoded file has its own) and Last-Modified, and conditional requests are answered with 304 or 412 as described in Caching & Conditional Requests. Vary: Accept-Encoding is always sent so shared caches keep the variants apart.