This is `canary` version of documentation. It's still under construction and review.
ZANREAL logoNEMO

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.

/app/auth/_middleware.ts
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.

/app/auth/_middleware.ts
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.

proxy.ts
import { createNEMO } from '@rescale/nemo';
import { RedisAdapter } from "@/lib/nemo/redis";

export const proxy = createNEMO(middlewares, globalMiddleware, {
  storage: () => new RedisAdapter()
});
middleware.ts
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.

proxy.ts
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]);
middleware.ts
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.

/app/auth/_middleware.ts
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.

/app/admin/_middleware.ts
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:

middleware.ts
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:

app/page.tsx
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:

middleware.ts
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:

middleware.ts
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:

proxy.ts
import { createNEMO } from '@rescale/nemo';

export const proxy = createNEMO(middlewares, globalMiddleware, {
  debug: true,        // Enable detailed logs
  enableTiming: true  // Enable performance measurements
});
middleware.ts
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.

proxy.ts
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]);
middleware.ts
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.

middleware.test.ts
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);
  });
});