Production is not a larger localhost. It is a different beast. Your code runs on unknown hardware, at scale, with real people depending on it. Treat it differently.
Pre-deployment Checklist
Before moving anything to production, ensure these are done:
- Environment validation: Your app should crash on startup if env vars are missing or invalid.
- Database migrations: Automate them. Never run migrations manually.
- Error tracking: Sentry, Rollbar, or similar. Silent errors kill businesses.
- Health checks: Implement /health endpoints that verify database connectivity.
Environment Configuration
Validate all environment variables at startup. Fail loudly if anything is missing:
// next.config.ts
import { z } from "zod";
const envSchema = z.object({
DATABASE_URL: z.string().url(),
REDIS_URL: z.string().url(),
NODE_ENV: z.enum(["development", "production", "test"]),
NEXT_PUBLIC_API_URL: z.string().url(),
});
const env = envSchema.parse(process.env);
export default {
// ... rest of config
};Connection Pooling
Never create a new database connection per request. Connection pools manage reuse efficiently:
// src/lib/db.ts
import { Pool } from "pg";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Max connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on("error", (error) => {
console.error("Unexpected error on idle client", error);
process.exit(-1);
});
export async function query(text: string, params?: any[]) {
const start = Date.now();
try {
const result = await pool.query(text, params);
const duration = Date.now() - start;
if (duration > 1000) {
console.warn(
`Slow query (${duration}ms): ${text}`
);
}
return result.rows;
} catch (error) {
console.error("Database error:", error);
throw error;
}
}Graceful Shutdown
When SIGTERM arrives, finish in-flight requests before exiting:
// main.ts
process.on("SIGTERM", async () => {
console.log("SIGTERM received. Shutting down gracefully...");
// Stop accepting new requests
server.close(() => {
console.log("HTTP server closed");
});
// Close database connections
await pool.end();
console.log("Database pool closed");
// Close any other resources
await redis.quit();
console.log("Redis connection closed");
// Exit
process.exit(0);
});Monitoring in Production
You cannot fix what you cannot see. Implement monitoring from day one:
// lib/monitor.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1, // 10% of transactions
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
});
export function captureException(error: unknown) {
Sentry.captureException(error);
}
// In your API routes
export async function GET(request: NextRequest) {
try {
const data = await fetchData();
return NextResponse.json(data);
} catch (error) {
captureException(error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}