Skip to content

Middleware

CAUTION

Experimental — not for production. Managed Data Mode SSR is available for evaluation and testing. Do not use it in production yet; the API and behaviour may change. In the meantime, continue using Webpack SSR.

SSR has three places to run middleware — pick the one that matches the job:

  1. Server middleware — production and start; request context before HTML render
  2. Config devServerMiddlewaresku start only; local mocks and proxies
  3. React Router middleware — isomorphic behaviour on matched routes

Server-entry middleware

Export Connect/Express handlers from the server entry. sku mounts them before the HTML render path in both sku start and production.

tsx
// src/server.tsx
import { defineServerEntry } from 'sku/runtime';

const server = defineServerEntry({
  middleware: [
    (req, res, next) => {
      req.user = { id: '…' }; // after Express Request module augmentation
      if (req.path === '/api/health') {
        res.status(200).type('text/plain').send('ok');
        return;
      }
      next();
    },
  ],
});

export default server;

Use this for production request handlers and for attaching values on req that entry getters (or server getRouterContext) will read.

Do not put raw Express req into React Router context — project values via dual-entry getRouterContext.

Typing middleware-attached fields on req

Fields you append in middleware (req.user, req.log, …) are not on Express’s stock Request type. Augment Express the same way sku does for getCspNonce.

Install @types/express-serve-static-core as a direct dependency, then:

ts
// e.g. src/types/express.d.ts (ensure included by tsconfig)
declare module 'express-serve-static-core' {
  interface Request {
    user?: { id: string };
    log?: { info: (msg: string) => void };
  }
}

That augmentation is shared by middleware, the getters, and server getRouterContext.

Dev-only mocks (devServerMiddleware)

Use config devServerMiddleware for local mocks and proxies that production never serves from the Node app (for example /api traffic a reverse proxy handles when deployed). sku mounts that file only in SSR sku start, never in the production server.

ts
import type { SkuConfig } from 'sku';

export default {
  bundler: 'vite',
  buildType: 'ssr',
  devServerMiddleware: './dev-middleware.js',
} satisfies SkuConfig;
js
export default (app) => {
  app.get('/mock-api', (_req, res) => {
    res.status(200).type('text/plain').send('ok');
  });
};

React Router route middleware

React Router Data Mode supports a middleware array on routes for isomorphic behaviour on matched routes. That is separate from Express middleware on the server entry — use Express for HTTP-level work, and route middleware for behaviour tied to the matched route tree.

See Routing → React Router route middleware and React Router’s middleware docs.

Mount order in production

  1. Request-context (sku; CSP nonce store, etc.)
  2. express.static for client assets under publicPath
  3. Server-entry middleware (optional)
  4. HTML render

Static mounts before server-entry middleware so catch-all URL-pattern handlers cannot eat hashed client assets under publicPath. App routes outside that prefix still reach middleware and HTML as usual.

Mount order in sku start

  1. Request-context (sku; CSP nonce store, etc.)
  2. Vite middlewares (HMR / module graph)
  3. Config devServerMiddleware (optional)
  4. Server-entry middleware
  5. HTML render

Document paths Vite does not handle still reach devServerMiddleware, server-entry middleware, and HTML. Dev-only mocks still mount before production middleware so they can intercept traffic that would never reach the app in production. sku start does not mount express.static under publicPath — Vite serves the module graph from /. Put anything that must ship in production on the server-entry export; keep stubs and local-only routes in devServerMiddleware.

See also