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:
- Server
middleware— production and start. Request context before HTML render - Config
devServerMiddleware—sku startonly. Local mocks and proxies - 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.
// 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:
// 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.
import type { SkuConfig } from 'sku';
export default {
bundler: 'vite',
buildType: 'ssr',
devServerMiddleware: './dev-middleware.js',
} satisfies SkuConfig;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
- Request-context (sku, CSP nonce store, etc.)
express.staticfor client assets underpublicPath- Server-entry
middleware(optional) - 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
- Request-context (sku, CSP nonce store, etc.)
- Vite middlewares (HMR / module graph)
- Config
devServerMiddleware(optional) - Server-entry
middleware - 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
- Request entries —
middlewareon the server entry - Data loading — project Express values into router context
- Logging — request access logs via Express middleware
- Deploy to production — stand-alone vs reverse-proxy asset serving
- Routing — route-level middleware
