Best practices
Best practices for developing Next.js middleware
Performance
Middleware performance is critical to the overall performance of your application. Whole middleware execution time should be as low as possible as it's executed before every request - which means it's directly affecting TTFB (Time To First Byte) of your application.
Concurrency
Minimize the number of blocking operations in your middleware. If you need to perform blocking operations, consider using concurrency to parallelize the operations.
import { NextMiddleware } from '@rescale/nemo';
export const auth: NextMiddleware = () => {
// Fetch user and roles concurrently
const [user, roles] = await Promise.all([
fetchUser(),
fetchRoles(),
]);
if(!user | !roles) {
return NextResponse.redirect('/login');
}
}Caching
Caching is a powerful technique to improve the performance of your middleware and reduce heavy operations like db queries. There are two types of caching you can use:
Cross-middleware caching
Use build-in storage to cache data that is used across multiple middleware functions in a chain.
This will reduce the number of requests to external services and reduce middleware exeuction time.
import { NextMiddleware } from '@rescale/nemo';
export const auth: NextMiddleware = (request, { storage }) => {
const [user, roles] = await Promise.all([
fetchUser(),
fetchRoles(),
]);
storage.set('user', user);
storage.set('roles', roles);
if(!user | !roles) {
return NextResponse.redirect('/login');
}
}Cross-requests caching
Build a custom adapter to cache data between requests using for example redis, Vercel Edge Config or other KV storage.
Warning! Keep this as fast as possible, as longer the middleware executes the longer the TTFB will be.
import { createNEMO } from '@rescale/nemo';
import { RedisAdapter } from "@/lib/nemo/redis";
export const proxy = createNEMO(middlewares, globalMiddleware, {
storage: () => new RedisAdapter()
});import { createNEMO } from '@rescale/nemo';
import { RedisAdapter } from "@/lib/nemo/redis";
export const middleware = createNEMO(middlewares, globalMiddleware, {
storage: () => new RedisAdapter()
});Security
Rate limiting
Implement rate limiting in your middleware to protect your application from abuse and potential DoS attacks. Rate limiting can be applied globally or to specific routes.
import { createNEMO, NextMiddleware } from '@rescale/nemo';
import { RateLimiter } from '@/lib/rate-limiter';
const rateLimiter: NextMiddleware = async (request, { storage }) => {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
const limiter = new RateLimiter();
const { success, limit, remaining, reset } = await limiter.check(ip);
if (!success) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString()
}
});
}
};
export const proxy = createNEMO([rateLimiter, ...otherMiddlewares]);import { createNEMO, NextMiddleware } from '@rescale/nemo';
import { RateLimiter } from '@/lib/rate-limiter';
const rateLimiter: NextMiddleware = async (request, { storage }) => {
const ip = request.ip || request.headers.get('x-forwarded-for') || 'unknown';
const limiter = new RateLimiter();
const { success, limit, remaining, reset } = await limiter.check(ip);
if (!success) {
return new Response('Too Many Requests', {
status: 429,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString()
}
});
}
};
export const middleware = createNEMO([rateLimiter, ...otherMiddlewares]);Authentication
Implement authentication checks early in your middleware chain to protect routes. Use storage to avoid redundant authentication checks in subsequent middleware functions.
import { NextMiddleware, NextResponse } from '@rescale/nemo';
import { verifyToken } from '@/lib/auth';
export const auth: NextMiddleware = async (request, { storage }) => {
const token = request.cookies.get('auth-token')?.value;
if (!token) {
return NextResponse.redirect('/login');
}
try {
// Verify token and get user
const user = await verifyToken(token);
// Store user in storage for other middleware to use
storage.set('user', user);
} catch (error) {
// Delete invalid token
const response = NextResponse.redirect('/login');
response.cookies.delete('auth-token');
return response;
}
};Authorization
Run authorization checks in middleware to control access to protected resources based on user roles and permissions.
import { NextMiddleware, NextResponse } from '@rescale/nemo';
export const adminOnly: NextMiddleware = (request, { storage }) => {
const user = storage.get('user');
if (!user || !user.roles.includes('admin')) {
return NextResponse.redirect('/unauthorized');
}
};Header forwarding
When you need to pass data from middleware to your page components, use header forwarding instead of storing everything in cookies or query parameters.
Best practices:
- Use descriptive header names with prefixes (e.g.,
x-app-locale,x-user-id) - Forward headers early in the middleware chain
- Keep forwarded headers minimal - only pass essential data
- Use storage for data that doesn't need to reach the page component
Example:
import { NextMiddleware, NextResponse } from '@rescale/nemo';
const localeMiddleware: NextMiddleware = async (request) => {
const locale = detectLocale(request);
// Forward locale to page components
return NextResponse.next({
request: {
headers: new Headers({
...Object.fromEntries(request.headers),
'x-locale': locale,
}),
},
});
};
const userMiddleware: NextMiddleware = async (request, { storage }) => {
const user = await getUser(request);
// Store user in storage for other middlewares
storage.set('user', user);
// Forward only user ID to page (not full user object)
return NextResponse.next({
request: {
headers: new Headers({
...Object.fromEntries(request.headers),
'x-user-id': user.id,
}),
},
});
};In your page component:
import { headers } from 'next/headers';
export default async function Page() {
const headersList = await headers();
const locale = headersList.get('x-locale');
const userId = headersList.get('x-user-id');
// Use the forwarded headers
return <div>Locale: {locale}, User ID: {userId}</div>;
}Skipping middlewares
Use event.skip() to conditionally skip remaining middlewares in the current chain section without returning a terminating response. This is useful for performance optimization and conditional execution.
When to use skip():
- Early exit when a condition is met (but you still want after-chain to run)
- Conditional middleware execution based on request properties
- Performance optimization by skipping unnecessary processing
- Skip cleanup logic when not needed
Basic usage:
import { NextMiddleware, NextResponse } from '@rescale/nemo';
const cacheCheck: NextMiddleware = async (request, { storage, event }) => {
const cacheKey = request.nextUrl.pathname;
const cached = storage.get(cacheKey);
if (cached) {
// Skip remaining middlewares, but allow after chain to run for cleanup
event.skip();
return NextResponse.next({
headers: {
'x-cached': 'true',
},
});
}
// Continue with processing if not cached
};
const expensiveOperation: NextMiddleware = async (request) => {
// This will not execute if skip() was called in cacheCheck
await performExpensiveOperation();
};Skip with after chain:
You can also skip the after chain by passing the skipAfter option:
import { NextMiddleware, NextResponse } from '@rescale/nemo';
const earlyExit: NextMiddleware = async (request, { event }) => {
if (request.headers.get('x-skip-all') === 'true') {
// Skip remaining middlewares AND after chain (no cleanup needed)
event.skip({ skipAfter: true });
return NextResponse.next();
}
// Continue with normal processing
};When to use skipAfter: true:
- When you want to completely bypass cleanup logic in the after chain
- When the request doesn't need any post-processing
- Performance optimization when you know no cleanup is needed
Reliability
Monitoring
NEMO provides built-in performance monitoring that you can easily enable through configuration options:
import { createNEMO } from '@rescale/nemo';
export const proxy = createNEMO(middlewares, globalMiddleware, {
debug: true, // Enable detailed logs
enableTiming: true // Enable performance measurements
});import { createNEMO } from '@rescale/nemo';
export const middleware = createNEMO(middlewares, globalMiddleware, {
debug: true, // Enable detailed logs
enableTiming: true // Enable performance measurements
});When enableTiming is enabled, NEMO will automatically:
- Track execution time for each middleware function
- Measure performance across different middleware chains (before, main, after)
- Log detailed timing information in the console
Logging
Implement structured logging in your middleware for better debugging and traceability.
import { createNEMO, NextMiddleware } from '@rescale/nemo';
import { logger } from '@/lib/logger';
const loggingMiddleware: NextMiddleware = async (request, { next, storage }) => {
const requestId = crypto.randomUUID();
const start = Date.now();
// Add request ID to storage for cross-middleware correlation
storage.set('requestId', requestId);
logger.info({
message: 'Request received',
requestId,
method: request.method,
path: request.nextUrl.pathname,
userAgent: request.headers.get('user-agent')
});
try {
const response = await next();
logger.info({
message: 'Request completed',
requestId,
status: response.status,
duration: Date.now() - start
});
return response;
} catch (error) {
logger.error({
message: 'Request failed',
requestId,
error: error.message,
stack: error.stack,
duration: Date.now() - start
});
throw error;
}
};
export const proxy = createNEMO([loggingMiddleware, ...otherMiddlewares]);import { createNEMO, NextMiddleware } from '@rescale/nemo';
import { logger } from '@/lib/logger';
const loggingMiddleware: NextMiddleware = async (request, { next, storage }) => {
const requestId = crypto.randomUUID();
const start = Date.now();
// Add request ID to storage for cross-middleware correlation
storage.set('requestId', requestId);
logger.info({
message: 'Request received',
requestId,
method: request.method,
path: request.nextUrl.pathname,
userAgent: request.headers.get('user-agent')
});
try {
const response = await next();
logger.info({
message: 'Request completed',
requestId,
status: response.status,
duration: Date.now() - start
});
return response;
} catch (error) {
logger.error({
message: 'Request failed',
requestId,
error: error.message,
stack: error.stack,
duration: Date.now() - start
});
throw error;
}
};
export const middleware = createNEMO([loggingMiddleware, ...otherMiddlewares]);Testing
Write comprehensive tests for your middleware to ensure reliability and catch regressions.
import { NextRequest } from 'next/server';
import { auth } from './app/auth/_middleware';
import { createMockStorage } from '@/lib/test-utils';
describe('Auth middleware', () => {
it('should redirect to login when no token is present', async () => {
// Arrange
const request = new NextRequest('https://example.com/dashboard');
const storage = createMockStorage();
// Act
const response = await auth(request, { storage, next: async () => new Response() });
// Assert
expect(response.status).toBe(307);
expect(response.headers.get('Location')).toBe('/login');
});
it('should proceed and store user when token is valid', async () => {
// Arrange
const request = new NextRequest('https://example.com/dashboard');
request.cookies.set('auth-token', 'valid-token');
const storage = createMockStorage();
const mockUser = { id: '123', name: 'Test User' };
// Mock verifyToken function
jest.mock('@/lib/auth', () => ({
verifyToken: jest.fn().mockResolvedValue(mockUser)
}));
// Act
const response = await auth(request, { storage, next: async () => new Response() });
// Assert
expect(response).toBeUndefined(); // No response means middleware passes through
expect(storage.get('user')).toEqual(mockUser);
});
});