Best approach to handle environment-specific API routes in Next.js 15? #84513
-
SummaryHey everyone 👋 I’m currently upgrading a project to Next.js 15 (App Router) and I’m wondering about the best way to manage environment-specific API routes. For example, I have a few API endpoints that behave differently in development, staging, and production — right now I’m using environment variables (process.env.NODE_ENV) inside the route handlers, but it’s starting to get messy as the project grows. I’m curious how other developers handle this scenario: Would love to hear how you organize environment logic cleanly in your Next.js projects 🙏 Additional informationNo response ExampleNo response |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
Halo! Great question - environment-specific API routes are a common challenge in Next.js projects. Let me share how our team approaches this in Next.js 15 with App Router: Current Approach: Environment Variables (The Messy Way) ❌What you're doing now: // pages/api/users/route.ts (or app/api/users/route.ts)
export async function GET(request: Request) {
if (process.env.NODE_ENV === 'development') {
// Mock data for development
return Response.json({ users: mockUsers });
} else if (process.env.NODE_ENV === 'staging') {
// Staging-specific logic
return Response.json({ users: stagingUsers });
} else {
// Production logic
return Response.json({ users: await fetchRealUsers() });
}
} This gets messy fast! Here are better approaches: 1. Environment-Based Route Splitting ✅We create separate route handlers for each environment:
// app/api/users/route.ts - Production default
export async function GET() {
const users = await fetchProductionUsers();
return Response.json({ users, environment: 'production' });
}
// app/api/users/route.dev.ts - Development override
export async function GET() {
const users = getMockUsers();
return Response.json({ users, environment: 'development' });
} Next.js automatically uses the environment-specific file based on 2. Configuration-Based Routing with Middleware ✅We use middleware to inject environment-specific config: // middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const environment = process.env.NODE_ENV;
// Clone the request headers and add environment context
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-environment', environment);
requestHeaders.set('x-api-version', process.env.API_VERSION || 'v1');
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
export const config = {
matcher: '/api/:path*',
}; Then in your route handlers: // app/api/users/route.ts
import { getEnvironmentConfig } from '@/lib/config';
export async function GET(request: Request) {
const config = getEnvironmentConfig(request.headers.get('x-environment'));
if (config.useMockData) {
return Response.json(await getMockUsers());
}
return Response.json(await fetchRealUsers(config.apiUrl));
} 3. Service Layer Pattern (Our Recommended Approach) ✅We separate business logic from route handlers:
// lib/config/environments.ts
export const environmentConfigs = {
development: {
apiBaseUrl: 'http://localhost:3001',
useMocks: true,
cacheTimeout: 0,
features: {
experimental: true,
analytics: false
}
},
staging: {
apiBaseUrl: 'https://staging-api.example.com',
useMocks: false,
cacheTimeout: 30000,
features: {
experimental: true,
analytics: true
}
},
production: {
apiBaseUrl: 'https://api.example.com',
useMocks: false,
cacheTimeout: 60000,
features: {
experimental: false,
analytics: true
}
}
};
export const getConfig = () => {
const env = process.env.NODE_ENV || 'development';
return environmentConfigs[env as keyof typeof environmentConfigs];
}; // lib/services/userService.ts
import { getConfig } from '@/lib/config/environments';
export class UserService {
private config = getConfig();
async getUsers() {
if (this.config.useMocks) {
return this.getMockUsers();
}
const response = await fetch(`${this.config.apiBaseUrl}/users`);
return response.json();
}
private getMockUsers() {
return { users: [{ id: 1, name: 'Mock User' }] };
}
} // app/api/users/route.ts - Clean route handler!
import { UserService } from '@/lib/services/userService';
export async function GET() {
const userService = new UserService();
const users = await userService.getUsers();
return Response.json(users);
} 4. API Versioning with Environment Prefixes ✅For more complex setups, we use URL-based routing: // app/api/[env]/users/route.ts
export async function GET(
request: Request,
{ params }: { params: { env: string } }
) {
const environment = params.env; // dev, staging, prod
// Validate allowed environments
const allowedEnvs = ['dev', 'staging', 'prod'];
if (!allowedEnvs.includes(environment)) {
return Response.json({ error: 'Invalid environment' }, { status: 400 });
}
const userService = new UserService(environment);
const users = await userService.getUsers();
return Response.json({
users,
environment,
timestamp: new Date().toISOString()
});
} 5. Feature Flags with Conditional Routing ✅We use feature flags for gradual rollouts: // lib/features/flags.ts
export const featureFlags = {
newUserAPI: {
development: true,
staging: true,
production: process.env.ENABLE_NEW_USER_API === 'true'
}
};
export const isFeatureEnabled = (feature: keyof typeof featureFlags) => {
const env = process.env.NODE_ENV as keyof typeof featureFlags.development;
return featureFlags[feature][env];
}; // app/api/users/route.ts
import { isFeatureEnabled } from '@/lib/features/flags';
export async function GET() {
if (isFeatureEnabled('newUserAPI')) {
return newUserAPIHandler();
} else {
return legacyUserAPIHandler();
}
} 6. Environment-Specific Middleware Stack ✅We compose middleware based on environment: // lib/middleware/compose.ts
import { withLogging } from './logging';
import { withAuth } from './auth';
import { withMock } from './mock';
import { withCaching } from './caching';
export const createMiddlewareStack = (handler: Function) => {
let stack = handler;
// Always add logging
stack = withLogging(stack);
// Environment-specific middleware
if (process.env.NODE_ENV === 'development') {
stack = withMock(stack);
}
if (process.env.NODE_ENV !== 'development') {
stack = withAuth(stack);
stack = withCaching(stack);
}
return stack;
}; Our Recommended Structure:
Key Takeaways:
Our team found that the Service Layer + Config Pattern works best for most projects. It keeps routes clean, makes testing easier, and scales well as your project grows. |
Beta Was this translation helpful? Give feedback.
Halo! Great question - environment-specific API routes are a common challenge in Next.js projects. Let me share how our team approaches this in Next.js 15 with App Router:
Current Approach: Environment Variables (The Messy Way) ❌
What you're doing now: