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. Until then, 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 devServerMiddleware — sku 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. Also use it to attach 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 };
  }
}

middleware, the getters, and server getRouterContext share that augmentation.

Dev-only mocks (devServerMiddleware) ​

Use config devServerMiddleware for local mocks and proxies. Production never serves those from the Node app. One example is /api traffic that 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. Use 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 intercept 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 exist in production on the server-entry export. Keep stubs and local-only routes in devServerMiddleware.

See also ​