Edupala

Comprehensive Full Stack Development Tutorial: Learn Ionic, Angular, React, React Native, and Node.js with JavaScript

Nextjs Building Resilient Web Applications: Handling API and Database Connection Issues

In modern web development, applications often rely heavily on external APIs or databases.These dependencies, however, can sometimes fail due to various issues such as network disruptions, IP address changes, or service outages. This guide will walk you through the process of creating a resilient web application that can gracefully handle such problems, with a special focus on Next.js applications using MongoDB.

1. Understanding the Challenge

When building applications dependent on external services, several issues can arise:

  • Database connection failures due to network problems or IP address changes
  • API endpoints becoming temporarily unavailable
  • Rate limiting or quota exhaustion
  • Slow responses leading to timeouts

A resilient application should manage these issues gracefully, ensuring a smooth user experience even when problems occur.

2. Implementing a Robust Database Connection

Let’s start by creating a robust database connection function, in our nextjs lib/dbConnect.ts file

Here is the actual code for robust mongodb connection.

import mongoose from "mongoose";

type ConnectionObject = {
  isConnected?: number;
};

const connection: ConnectionObject = {};

async function dbConnect(): Promise<void> {
  if (connection.isConnected) {
    console.log("Already connected to database");
    return;
  }

  const MONGODB_URI = process.env.MONGODB_URI;
  if (!MONGODB_URI) {
    throw new Error(
      "Please define the MONGODB_URI environment variable inside .env.local"
    );
  }

  const MAX_RETRIES = 3;
  let retries = 0;

  while (retries < MAX_RETRIES) {
    try {
      const db = await mongoose.connect(MONGODB_URI);
      connection.isConnected = db.connections[0].readyState;
      console.log("DB connection successful");
      return;
    } catch (error) {
      console.error(
        `Database connection attempt ${retries + 1} failed:`,
        error
      );
      retries++;
      if (retries === MAX_RETRIES) {
        console.error(
          "Max retries reached. Unable to connect to the database."
        );
        throw new Error(
          "Failed to connect to the database after multiple attempts"
        );
      }
      // Wait for 5 seconds before trying again
      await new Promise((resolve) => setTimeout(resolve, 5000));
    }
  }
}

export default dbConnect;

This function includes following approach:

  • Retry logic with a maximum of 3 attempts
  • A delay between retry attempts
  • Detailed error logging
  • Throwing an error instead of crashing the application

3. Creating a Resilient API Layer

Next, let’s create an API route that uses our robust database connection, here is mine home page it display list of products,

// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
import dbConnect from "@/lib/dbConnect";
import ProductModel from "@/model/Product";
import { createApiResponse, ApiResponse } from "@/types/ApiResponse";
import { IFoodItem } from "@/types";

export async function GET(req: NextRequest) {
  try {
    await dbConnect();

    const page = parseInt(req.nextUrl.searchParams.get("page") || "1");
    const limit = parseInt(req.nextUrl.searchParams.get("limit") || "12");
    const skip = (page - 1) * limit;

    const products = await ProductModel.find().skip(skip).limit(limit).lean();
    const totalProducts = await ProductModel.countDocuments();

    return NextResponse.json(
      {
        success: true,
        message:
          products.length > 0
            ? "Products retrieved successfully"
            : "No products found",
        products: products ? products : [],
        pagination: {
          currentPage: page,
          totalPages: Math.ceil(totalProducts / limit),
          totalProducts,
        },
      },
      { status: 200 }
    );
  } catch (error) {
    console.error("Error in /api/products:", error);

    return createApiResponse(
      false,
      "Unable to retrieve products at this time. Please check your database connection.",
      500
    );
  }
}

Where the createApiResponse function is inside the types/ApiResponse.ts file

import { NextResponse } from "next/server";
import { IFoodItem, IComment, IOrder } from ".";

// Base API response interface

export interface ApiResponse<T = undefined> {
  success: boolean;
  message: string;
  data?: T;
}

// Type aliases for specific response types
export type ApiProductResponse = ApiResponse<IFoodItem>;
export type ApiProductsResponse = ApiResponse<IFoodItem[]>;
export type ApiCommentResponse = ApiResponse<IComment>;
export type ApiCommentsResponse = ApiResponse<IComment[]>;
export type ApiOrderResponse = ApiResponse<IOrder>;
export type ApiOrdersResponse = ApiResponse<IOrder[]>;

// Helper function to create API responses
export function createApiResponse<T>(
  success: boolean,
  message: string,
  status: number,
  data?: T
): NextResponse<ApiResponse<T>> {
  return NextResponse.json<ApiResponse<T>>(
    { success, message, data },
    { status }
  );

This API route:

  • Utilizes the robust dbConnect function
  • Manages errors gracefully
  • Returns consistent responses using a createApiResponse helper function

4. Handling Errors in the Frontend

In your frontend components, implement error handling and provide feedback to users:

// components/HomePageClient.tsx

import React, { useState, useEffect } from "react";
import axios from "axios";

export default function HomePageClient({ initialData }: HomePageClientProps) {
  const [products, setProducts] = useState<IFoodItem[]>(initialData.products);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const fetchProducts = async () => {
    setIsLoading(true);
    setError(null);
    try {
      const response = await axios.get("/api/products");
      setProducts(response.data.data.products);
    } catch (error) {
      setError("Unable to load products. Please try again later.");
      console.error("Error fetching products:", error);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    if (products.length === 0) {
      fetchProducts();
    }
  }, []);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>{error}</div>;

  return (
    // Render your products here
  );
}

This component:

  • Manages loading states
  • Displays error messages to the user
  • Allows retrying the data fetch

5. Best Practices for Production

When deploying your application to production, consider the following best practices:

  1. Use environment variables for sensitive information like database URIs.
  2. Implement proper logging and monitoring to quickly identify and address issues.
  3. Consider using a service like Sentry for error tracking and real-time alerts.
  4. Implement rate limiting and caching strategies to reduce load on your database.
  5. Use a Content Delivery Network (CDN) to serve static assets and reduce latency.
  6. Regularly update your dependencies to ensure you have the latest security patches.

By following these practices and implementing robust error handling, you can create a resilient web application that provides a smooth user experience even when faced with external service issues.

Remember, building a resilient application is an ongoing process. Continuously monitor your application’s performance and user feedback to identify areas for improvement and refinement.

Nextjs Building Resilient Web Applications: Handling API and Database Connection Issues

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top