Juniper supports two types of middleware:
- Server Middleware (Hono) - Runs on every HTTP request on the server
- Client Middleware (React Router) - Runs during client-side navigation
Both can set values on the context object and transform requests/responses.
Server middleware uses Hono's middleware system and runs on every HTTP request.
Define middleware in your main.ts file using app.use():
// routes/main.ts
import { Hono } from "hono";
import type { AppEnv } from "@udibo/juniper/server";
const app = new Hono<AppEnv>();
// Middleware that runs on all requests
app.use(async (c, next) => {
const start = Date.now();
await next(); // Call downstream handlers
// After response
const duration = Date.now() - start;
console.log(`Request took ${duration}ms`);
});
export default app;Access the Juniper context from the Hono context:
app.use(async (c, next) => {
const context = c.get("context"); // RouterContextProvider
// Set values for downstream loaders and components
const user = await getUser(c.req.header("Authorization"));
context.set(userContext, user);
await next();
});Hono provides many built-in middleware modules:
import { Hono } from "hono";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";
import { compress } from "hono/compress";
import { timing } from "hono/timing";
const app = new Hono();
// Request/response logging
app.use(logger());
// CORS headers
app.use(cors({
origin: ["https://example.com"],
allowMethods: ["GET", "POST", "PUT", "DELETE"],
}));
// Security headers
app.use(secureHeaders());
// Response compression
app.use(compress());
// Server timing headers
app.use(timing());See Hono's middleware documentation for the full list.
Middleware executes in the order defined. Earlier middleware wraps later middleware:
app.use(first); // Runs first, completes last
app.use(second); // Runs second
app.use(third); // Runs third, completes first
// Execution order:
// first (before next) → second (before next) → third
// third (after next) → second (after next) → first (after next)Path-specific middleware runs only for matching routes:
// All routes
app.use(logger());
// Only /api/* routes
app.use("/api/*", cors());
// Only /admin/* routes
app.use("/admin/*", requireAdmin);Set user context from authentication tokens:
// routes/main.ts
import { Hono } from "hono";
import { createContext } from "react-router";
import type { AppEnv } from "@udibo/juniper/server";
export interface User {
id: string;
name: string;
email: string;
}
export const userContext = createContext<User | null>();
const app = new Hono<AppEnv>();
app.use(async (c, next) => {
const context = c.get("context");
const token = c.req.header("Authorization")?.replace("Bearer ", "");
let user: User | null = null;
if (token) {
user = await verifyToken(token);
}
context.set(userContext, user);
await next();
});
export default app;Protect routes that require authentication:
// routes/admin/main.ts
import { HttpError } from "@udibo/juniper";
import { Hono } from "hono";
import type { AppEnv } from "@udibo/juniper/server";
import { userContext } from "../main.ts";
const app = new Hono<AppEnv>();
app.use(async (c, next) => {
const context = c.get("context");
const user = context.get(userContext);
if (!user) {
throw new HttpError(401, "Authentication required");
}
if (!user.isAdmin) {
throw new HttpError(403, "Admin access required");
}
await next();
});
export default app;Use Hono's built-in logger:
import { logger } from "hono/logger";
app.use(logger());
// Output: <-- GET /blog
// --> GET /blog 200 12msCustom logging with request IDs:
app.use(async (c, next) => {
const requestId = crypto.randomUUID();
const start = Date.now();
console.log(`[${requestId}] ${c.req.method} ${c.req.url}`);
await next();
const duration = Date.now() - start;
console.log(`[${requestId}] ${c.res.status} ${duration}ms`);
});Validate content type for API routes:
import { HttpError } from "@udibo/juniper";
app.use("/api/*", async (c, next) => {
const contentType = c.req.header("Content-Type");
if (c.req.method !== "GET" && !contentType?.includes("application/json")) {
throw new HttpError(415, "Content-Type must be application/json");
}
await next();
});Rate limiting:
const requestCounts = new Map<string, number>();
app.use(async (c, next) => {
const ip = c.req.header("x-forwarded-for") || "unknown";
const count = requestCounts.get(ip) || 0;
if (count >= 100) {
throw new HttpError(429, "Too many requests");
}
requestCounts.set(ip, count + 1);
await next();
});Client middleware is defined in .tsx files and runs during client-side
navigation.
Export a middleware array from your route file:
// routes/dashboard/index.tsx
import type { MiddlewareFunction } from "@udibo/juniper";
export const middleware: MiddlewareFunction[] = [
async ({ context, request }) => {
console.log("Dashboard middleware running");
// Set context values
context.set(dashboardContext, { loadedAt: new Date() });
// next() is called automatically after this middleware completes
},
];
export default function Dashboard() {
return <h1>Dashboard</h1>;
}Client middleware receives these arguments:
interface MiddlewareArgs {
context: RouterContextProvider; // Shared context object
request: Request; // The current request
params: Record<string, string>; // Route parameters
}Example with all arguments:
export const middleware: MiddlewareFunction[] = [
async ({ context, request, params }) => {
console.log(`Route: ${params.id}`);
console.log(`URL: ${request.url}`);
// Set context for downstream components
context.set(myContext, { timestamp: Date.now() });
},
];If you don't call next(), it's called automatically after your middleware
completes. You only need to call next() explicitly when you want to run code
after child routes and their handlers have executed:
export const middleware: MiddlewareFunction[] = [
async ({ context }, next) => {
// Before downstream handlers
const start = performance.now();
await next(); // Execute child middleware, loaders, actions, render
// After downstream handlers complete
const duration = performance.now() - start;
console.log(`Route took ${duration}ms`);
},
];To short-circuit and prevent downstream execution, throw a redirect or error:
import { redirect } from "react-router";
export const middleware: MiddlewareFunction[] = [
async ({ context }) => {
const user = context.get(userContext);
if (!user) {
throw redirect("/login"); // Prevents downstream execution
}
// If user exists, next() is called automatically
},
];Redirect unauthenticated users to login:
import { redirect } from "react-router";
import type { MiddlewareFunction } from "@udibo/juniper";
import { userContext } from "@/context/user.ts";
export const middleware: MiddlewareFunction[] = [
async ({ context }) => {
const user = context.get(userContext);
if (!user) {
throw redirect("/login");
}
},
];Track page views for analytics:
import type { MiddlewareFunction } from "@udibo/juniper";
export const middleware: MiddlewareFunction[] = [
async ({ request }) => {
// Send to analytics service
analytics.trackPageView(request.url);
},
];Check feature flags before rendering:
import { redirect } from "react-router";
import type { MiddlewareFunction } from "@udibo/juniper";
import { featureFlagsContext } from "@/context/features.ts";
export const middleware: MiddlewareFunction[] = [
async ({ context }) => {
const flags = context.get(featureFlagsContext);
if (!flags?.newDashboard) {
throw redirect("/dashboard-old");
}
},
];Measure how long routes take to load:
import type { MiddlewareFunction } from "@udibo/juniper";
export const middleware: MiddlewareFunction[] = [
async ({ request }, next) => {
const start = performance.now();
await next();
const duration = performance.now() - start;
console.log(`${request.url} loaded in ${duration}ms`);
},
];| Scenario | Server Middleware | Client Middleware |
|---|---|---|
| Initial page load (SSR) | Yes | No |
| Client-side navigation | No | Yes |
| Form submission to server | Yes | No |
| Direct URL access | Yes | No |
Use server middleware for:
- Authentication token verification
- Request logging
- CORS and security headers
- Server-side request validation
Use client middleware for:
- Client-side auth state checks
- Navigation tracking
- Client-side feature flags
- UI-related context setup
Next: Forms - Form handling with client and server actions
Related topics:
- State Management - Sharing data across your app
- Error Handling - Error boundaries and HttpError
- Logging - Logging and OpenTelemetry