Back to posts

Refactor database connection with reliable singleton pattern for Next.js 15

This commit refactors the database connection implementation by introducing a robust singleton pattern, ensuring reliable and consistent connections throughout the application in Next.js 15.

Drizzle ORMNext JS 15NextJS
AG
Ala GARBAA 🚀 Full-Stack & DevOps Engineer

lib/server/db/index.ts


import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as dotenv from "dotenv";
import * as schema from "./schema";
// Load environment variables from .dev.env
dotenv.config({ path: "./.dev.env" });
/**
 * Database connection singleton implementation
 * This ensures the connection pool is initialized only once across the application
 *
 * This implementation follows the recommended pattern for Next.js applications
 * to prevent connection pool exhaustion during development and production.
 */
// Define a more specific global object to avoid namespace conflicts
// This is a key improvement for Next.js module caching
const globalForDb = global as unknown as {
  pgConnection: {
    client?: ReturnType;
    db?: ReturnType;
    connectionId?: string;
    isConnected: boolean;
  };
};
// Initialize the global object if it doesn't exist
if (!globalForDb.pgConnection) {
  globalForDb.pgConnection = {
    client: undefined,
    db: undefined,
    connectionId: undefined,
    isConnected: false,
  };
}
// Check for required environment variables
if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL environment variable is not defined");
}
// Configure connection pool options based on environment
const connectionOptions = {
  max: process.env.NODE_ENV === "production" ? 20 : 5, // Smaller pool for development
  idle_timeout: 30, // Connection idle timeout in seconds
  connect_timeout: 10, // Connection timeout in seconds
  // Add additional options as needed
};
// Function to create a new database connection with a unique ID for tracking
function createDbConnection() {
  try {
    console.log("[Database] Initializing database connection...");
    // Create the PostgreSQL client with the configured options
    const client = postgres(
      process.env.DATABASE_URL as string,
      connectionOptions
    );
    console.log("[Database] PostgreSQL client created");
    // Initialize Drizzle ORM with the client and schema
    const db = drizzle(client, {
      schema, // Pass the schema for better type safety and relations
      logger: process.env.NODE_ENV !== "production", // Enable logging in development
    });
    console.log("[Database] Drizzle ORM initialized");
    // Generate a unique connection ID for debugging
    const connectionId = Math.random().toString(36).substring(2, 10);
    console.log(`[Database] Connection ID: ${connectionId}`);
    return { client, db, connectionId };
  } catch (error) {
    console.error(
      "[Database] Failed to initialize database connection:",
      error
    );
    throw error;
  }
}
// Get or create the database connection
let db: ReturnType;
// This is the key part - we use a more reliable approach to singleton management
if (process.env.NODE_ENV === "production") {
  // In production, we create a new connection for each serverless function instance
  // but reuse it for subsequent requests to that instance
  console.log("[Database] Production environment detected");
  if (!globalForDb.pgConnection.isConnected) {
    const conn = createDbConnection();
    globalForDb.pgConnection.client = conn.client;
    globalForDb.pgConnection.db = conn.db;
    globalForDb.pgConnection.connectionId = conn.connectionId;
    globalForDb.pgConnection.isConnected = true;
    console.log(
      `[Database] Created new production connection with ID: ${conn.connectionId}`
    );
    db = conn.db;
  } else {
    console.log(
      `[Database] Reusing production connection with ID: ${globalForDb.pgConnection.connectionId}`
    );
    db = globalForDb.pgConnection.db!;
  }
} else {
  // In development, we want to reuse the connection across hot reloads
  console.log("[Database] Development environment detected");
  if (!globalForDb.pgConnection.isConnected) {
    const conn = createDbConnection();
    globalForDb.pgConnection.client = conn.client;
    globalForDb.pgConnection.db = conn.db;
    globalForDb.pgConnection.connectionId = conn.connectionId;
    globalForDb.pgConnection.isConnected = true;
    console.log(
      `[Database] Created new development connection with ID: ${conn.connectionId}`
    );
    db = conn.db;
  } else {
    console.log(
      `[Database] Reusing development connection with ID: ${globalForDb.pgConnection.connectionId}`
    );
    db = globalForDb.pgConnection.db!;
  }
}
console.log("[Database] Database connection ready");
/**
 * Helper function to check if we're using a cached connection
 * You can call this in your API routes or server components to debug
 */
export function getConnectionStatus() {
  return {
    isConnected: globalForDb.pgConnection.isConnected,
    connectionId: globalForDb.pgConnection.connectionId,
    environment: process.env.NODE_ENV || "unknown",
    timestamp: new Date().toISOString(),
  };
}
export { db };
/**
 * Best practices for connection pooling with postgres and Drizzle ORM:
 *
 * 1. Use a singleton pattern (implemented above) to ensure only one connection pool exists
 * 2. Configure appropriate pool size based on your application's needs:
 *    - For production, start with ~20 connections and adjust based on monitoring
 *    - For development, a smaller pool (1-5) is sufficient
 * 3. Consider using a connection pooler like PgBouncer for high-traffic applications
 * 4. Implement proper error handling and connection retry logic
 * 5. For serverless environments, consider specialized connection management approaches
 * 6. Implement graceful shutdown to properly close connections when the app terminates
 *
 * Future improvements:
 * - Add connection health checks
 * - Implement retry logic for transient connection failures
 * - Add metrics collection for connection pool usage
 * - Consider implementing a connection pooler for production environments
 */

app/debug/db-status/page.tsx


"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/uix/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/uix/card";
import { Badge } from "@/components/uix/badge";
interface ConnectionStatus {
  isConnected: boolean;
  connectionId?: string;
  environment: string;
  timestamp: string;
  connectionTest?: {
    success: boolean;
    queryTime: string;
    result: any;
  };
  requestTime: string;
}
interface ApiResponse {
  status: string;
  message: string;
  data: ConnectionStatus;
}
export default function DbStatusPage() {
  const [status, setStatus] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [refreshCount, setRefreshCount] = useState(0);
  useEffect(() => {
    async function fetchStatus() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch("/api/debug/db-status");
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data: ApiResponse = await response.json();
        setStatus(data.data);
      } catch (err) {
        setError(
          err instanceof Error ? err.message : "An unknown error occurred"
        );
        console.error("Error fetching DB status:", err);
      } finally {
        setLoading(false);
      }
    }
    fetchStatus();
  }, [refreshCount]);
  const handleRefresh = () => {
    setRefreshCount((prev) => prev + 1);
  };
  return (
    

Database Connection Status

Refresh count: {refreshCount}
{error && (

Error:

{error}

)} {status && (
Connection Status Current database connection information
Connection Status: {status.isConnected ? "Connected" : "Disconnected"}
Connection ID: {status.connectionId || "N/A"}
Environment: {status.environment}
Last Updated: {new Date(status.timestamp).toLocaleString()}
Connection Test Results from test query {status.connectionTest ? (
Test Status: {status.connectionTest.success ? "Success" : "Failed"}
Query Time: {status.connectionTest.queryTime}
Query Result:
                      {JSON.stringify(status.connectionTest.result, null, 2)}
                    
) : (

No connection test data available

)}
)}

How to Use This Page

  • This page helps you verify that your database connection singleton is working correctly
  • Click Refresh Status multiple times to check if the same connection is being reused
  • The Connection ID should remain the same across refreshes in development mode
  • Check the server logs to see if new connections are being created
); }

app/api/debug/db-status/route.ts


import { NextResponse } from "next/server";
import { getConnectionStatus, db } from "@/lib/server/db";
import { sql } from "drizzle-orm";
export async function GET() {
  try {
    // Get the current connection status
    const status = getConnectionStatus();
    // Perform a simple query to test the connection
    const startTime = performance.now();
    const testResult = await db.execute(sql`SELECT 1 as connection_test`);
    const queryTime = performance.now() - startTime;
    // Return the status as JSON
    return NextResponse.json({
      status: "success",
      message: "Database connection status",
      data: {
        ...status,
        connectionTest: {
          success: true,
          queryTime: `${queryTime.toFixed(2)}ms`,
          result: testResult,
        },
        requestTime: new Date().toISOString(),
      },
    });
  } catch (error) {
    console.error("[API] Error fetching database status:", error);
    return NextResponse.json(
      {
        status: "error",
        message: "Failed to fetch database status",
        error: error instanceof Error ? error.message : String(error),
      },
      { status: 500 }
    );
  }
}

app/api/debug/db-test/route.ts


import { NextResponse } from "next/server";
import { db } from "@/lib/server/db";
import { sql } from "drizzle-orm";
export async function GET() {
  try {
    // Perform a simple query to test the database connection
    const startTime = performance.now();
    // Simple query that doesn't require any tables
    const result = await db.execute(sql`SELECT 1 as test`);
    const endTime = performance.now();
    const queryTime = endTime - startTime;
    // Return the result as JSON
    return NextResponse.json({
      status: "success",
      message: "Database connection test successful",
      data: {
        result,
        queryTime: `${queryTime.toFixed(2)}ms`,
        timestamp: new Date().toISOString(),
      },
    });
  } catch (error) {
    console.error("[API] Error testing database connection:", error);
    return NextResponse.json(
      {
        status: "error",
        message: "Database connection test failed",
        error: error instanceof Error ? error.message : String(error),
      },
      { status: 500 }
    );
  }
}

Released under the MIT License. Ala GARBAA © 2009-2025.

Built & designed by Ala GARBAA. RSS Feed