Next.js course · Module 4: Databases and Data Fetching

Overview of Database Options for Next.js (SQL, NoSQL)

7 min read ·
In this lesson5

Choosing the right database is one of the key decisions when building a Next.js application. In this module, we will explore the most popular database options along with their advantages and disadvantages in the context of Next.js applications.

Types of Databases

Databases can be divided into two main categories: SQL (relational) and NoSQL (non-relational). Each has its own characteristic features that determine which projects they are best suited for.

SQL Databases (Relational)

Relational databases organize data in tables with a predefined structure and relationships between them. Examples of popular SQL databases include:

  1. PostgreSQL

    • Free, open-source
    • Advanced features (e.g., JSON types, full-text search)
    • Excellent performance and scalability
    • Excellent data integrity
  2. MySQL/MariaDB

    • Widely used, free database
    • Good performance for read operations
    • Lower resource requirements than PostgreSQL
  3. SQLite

    • Lightweight database (file) without a separate server
    • Ideal for prototyping and testing
    • Not suitable for production applications with high traffic

NoSQL Databases (Non-relational)

NoSQL databases offer greater flexibility in data structure and typically better horizontal scalability. The main types are:

  1. Document (MongoDB, Firebase Firestore)

    • Store data as documents (JSON/BSON objects)
    • Flexible structure without a rigid schema
    • Great for rapid prototyping and unstructured data
  2. Key-Value (Redis, DynamoDB)

    • Store simple key-value pairs
    • Extremely fast read/write operations
    • Often used as cache
  3. Wide-Column (Cassandra, HBase)

    • Store data in columns instead of rows
    • High scalability and performance for huge datasets
  4. Graph (Neo4j, Amazon Neptune)

    • Specialize in relationships between data
    • Ideal for complex relationships and connection analysis

1. PostgreSQL + Prisma

PostgreSQL combined with the Prisma ORM is one of the most popular options for Next.js applications:

// Example Prisma model for PostgreSQL
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Profile {
  id     Int    @id @default(autoincrement())
  bio    String
  user   User   @relation(fields: [userId], references: [id])
  userId Int    @unique
}

Advantages:

  • Strong data integrity through relationships
  • High performance and reliability
  • Good scalability
  • Excellent TypeScript support thanks to Prisma

Disadvantages:

  • Requires a database server
  • Rigid data structure (schema)

2. MongoDB + Mongoose

MongoDB is a popular non-relational document database, often used with Next.js:

// Example Mongoose model for MongoDB
import mongoose from 'mongoose';

const UserSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Please provide a name'],
    maxlength: [50, 'Name cannot be longer than 50 characters']
  },
  email: {
    type: String,
    required: [true, 'Please provide an email'],
    unique: true,
    match: [
      /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/,
      'Please provide a valid email address'
    ]
  },
  posts: [
    {
      type: mongoose.Schema.Types.ObjectId,
      ref: 'Post'
    }
  ],
  createdAt: {
    type: Date,
    default: Date.now
  }
});

export default mongoose.models.User || mongoose.model('User', UserSchema);

Pros:

  • Flexible data structure
  • Easy horizontal scalability
  • Data format similar to JSON
  • Good TypeScript support

Cons:

  • Weaker data integrity than in SQL databases
  • More difficult complex queries

3. Serverless Databases (PlanetScale, Neon, Supabase)

Serverless databases are a new trend that perfectly fits the Next.js architecture:

// Example of using PlanetScale with Next.js
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// In API route or Server Component
export async function getUsers() {
  try {
    const users = await prisma.user.findMany({
      include: { posts: true }
    });
    return users;
  } catch (error) {
    console.error('Database error:', error);
    throw new Error('Failed to fetch users');
  }
}

Popular options:

  • PlanetScale - MySQL-compatible serverless database
  • Neon - PostgreSQL in a serverless model
  • Supabase - backend-as-a-service platform with PostgreSQL
  • Firebase - comprehensive solution with Firestore as a document database

Pros:

  • No need to manage infrastructure
  • Pay-per-use (you only pay for what you use)
  • Automatic scaling
  • Often free tier for small projects

Cons:

  • Potentially higher costs at large scale
  • Less control over infrastructure
  • Dependency on an external provider

4. Edge Databases (Cloudflare D1, Fauna)

A novelty in the Next.js ecosystem are databases running on the edge:

// Example of using Cloudflare D1 with Next.js
// In API Route (Pages Router) or Route Handler (App Router)
import { D1Database } from '@cloudflare/workers-types';

export const runtime = 'edge';

export async function GET() {
  const db = process.env.DB as unknown as D1Database;

  const { results } = await db
    .prepare('SELECT * FROM users WHERE status = ?')
    .bind('active')
    .all();

  return Response.json({ users: results });
}

Pros:

  • Minimal latency for global users
  • Integration with edge functions
  • Often lower costs

Cons:

  • Limited capabilities compared to traditional databases
  • Relatively new technology

Choosing a Database for Next.js - Factors to Consider

1. Data Structure

  • If you have a clearly defined data structure with relationships → PostgreSQL or MySQL
  • If the data structure is fluid and may change → MongoDB or Firestore

2. Scalability

  • For applications that need to handle millions of users → Serverless options (PlanetScale, Neon)
  • For applications with predictable traffic → Traditional solutions like PostgreSQL

3. Team and Experience

  • If the team has SQL experience → PostgreSQL/MySQL
  • If the team prefers a JavaScript-first approach → MongoDB

4. Hosting and Deployment

  • If you use Vercel for Next.js hosting → Integrates well with PlanetScale, Neon, Supabase
  • If you use Cloudflare Pages → Cloudflare D1 or Cloudflare KV

Integration Examples in Next.js

Example 1: PostgreSQL + Prisma in Next.js

// lib/prisma.ts
import { PrismaClient } from '@prisma/client';

let prisma: PrismaClient;

if (process.env.NODE_ENV === 'production') {
  prisma = new PrismaClient();
} else {
  // Preventing multiple instances in dev mode
  if (!global.prisma) {
    global.prisma = new PrismaClient();
  }
  prisma = global.prisma;
}

export default prisma;

// In Server Component (App Router)
// app/users/page.tsx
import prisma from '@/lib/prisma';

export default async function UsersPage() {
  const users = await prisma.user.findMany({
    include: {
      posts: true,
    },
  });

  return (
    <div>
      <h1>Users</h1>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} - {user.posts.length} posts
          </li>
        ))}
      </ul>
    </div>
  );
}

Example 2: MongoDB in Next.js (Pages Router)

// lib/mongodb.js
import { MongoClient } from 'mongodb';

const MONGODB_URI = process.env.MONGODB_URI;
const MONGODB_DB = process.env.MONGODB_DB;

if (!MONGODB_URI) {
  throw new Error('Define MONGODB_URI in .env.local file');
}

if (!MONGODB_DB) {
  throw new Error('Define MONGODB_DB in .env.local file');
}

let cachedClient = null;
let cachedDb = null;

export async function connectToDatabase() {
  if (cachedClient && cachedDb) {
    return { client: cachedClient, db: cachedDb };
  }

  const client = await MongoClient.connect(MONGODB_URI, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  });

  const db = await client.db(MONGODB_DB);

  cachedClient = client;
  cachedDb = db;

  return { client, db };
}

// In API Route
// pages/api/users.js
import { connectToDatabase } from '@/lib/mongodb';

export default async function handler(req, res) {
  const { db } = await connectToDatabase();

  const users = await db
    .collection('users')
    .find({})
    .limit(20)
    .toArray();

  res.status(200).json(users);
}

Summary

When choosing a database for Next.js, it is worth following these guidelines:

  1. Applications with clearly defined relationships: PostgreSQL + Prisma
  2. Rapid prototyping and flexible data structures: MongoDB or Firestore
  3. Serverless applications: PlanetScale, Neon, or Supabase
  4. Global applications with low latency: Edge Databases like Cloudflare D1 or FaunaDB

Regardless of the choice, Next.js offers excellent integration mechanisms with all popular databases. In the next module, we will learn how to configure an ORM for the chosen solution to further streamline working with the database.

Code for this lesson: app/page.tsx
// Database Integration Demo - app/page.tsx
'use client';

import { useState, useEffect } from 'react';
import { CitizenManager } from './components/CitizenManager';
import { EnergyMonitor } from './components/EnergyMonitor';
import { DatabaseStatsPanel } from './components/DatabaseStatsPanel';
import { QueryBuilder } from './components/QueryBuilder';

interface DatabaseStats {
  totalCitizens: number;
  activeConnections: number;
  energyRecords: number;
  avgResponseTime: number;
  lastBackup: string;
}

export default function DatabaseHomePage() {
  const [activeTab, setActiveTab] = useState('overview');
  const [isConnected, setIsConnected] = useState(false);
  const [connectionStatus, setConnectionStatus] = useState('connecting');
  const [stats, setStats] = useState<DatabaseStats>({
    totalCitizens: 2847392,
    activeConnections: 47,
    energyRecords: 9542816,
    avgResponseTime: 23.7,
    lastBackup: '2024-01-15 03:00:00'
  });

  useEffect(() => {
    console.log("Initializing database connection...");

    // Simulate database connection
    const connectToDatabase = async () => {
      setConnectionStatus('connecting');

      // Simulate connection delay
      await new Promise(resolve => setTimeout(resolve, 2000));

      setIsConnected(true);
      setConnectionStatus('connected');
      console.log("Database connection established");

      // Simulate stats updates
      const interval = setInterval(() => {
        setStats(prev => ({
          ...prev,
          activeConnections: prev.activeConnections + Math.floor(Math.random() * 3) - 1,
          avgResponseTime: Math.max(10, prev.avgResponseTime + (Math.random() - 0.5) * 5)
        }));
      }, 3000);

      return () => clearInterval(interval);
    };

    connectToDatabase();
  }, []);

  const getConnectionColor = () => {
    switch (connectionStatus) {
      case 'connected': return '#4caf50';
      case 'connecting': return '#ff9800';
      case 'error': return '#f44336';
      default: return '#757575';
    }
  };

  const getConnectionIcon = () => {
    switch (connectionStatus) {
      case 'connected': return 'OK';
      case 'connecting': return '↻';
      case 'error': return '×';
      default: return '⏳';
    }
  };

  return (
    <div className="database-home">
      <section className="hero-section">
        <h1>Quantum Database Control Center</h1>
        <p>
          Manage Metropolis Quantum data using modern
          database technologies and ORM in Next.js 16
        </p>

        <div className="connection-status">
          <div
            className="status-indicator"
            style={{ backgroundColor: getConnectionColor() }}
          >
            <span className="status-icon">{getConnectionIcon()}</span>
            <span className="status-text">
              Database: {connectionStatus.toUpperCase()}
            </span>
          </div>

          {isConnected && (
            <div className="connection-details">
              <span>Quantum-DB v3.2.1</span>
              <span>Cluster: metropolis-primary</span>
              <span>Response: {stats.avgResponseTime.toFixed(1)}ms</span>
            </div>
          )}
        </div>
      </section>

      <section className="database-overview">
        <h2>Database Overview</h2>

        <div className="overview-grid">
          <div className="overview-card">
            <div className="card-icon">◆</div>
            <div className="card-content">
              <div className="card-value">{stats.totalCitizens.toLocaleString()}</div>
              <div className="card-label">Total Citizens</div>
              <div className="card-change positive">+127 today</div>
            </div>
          </div>

          <div className="overview-card">
            <div className="card-icon">↯</div>
            <div className="card-content">
              <div className="card-value">{stats.energyRecords.toLocaleString()}</div>
              <div className="card-label">Energy Records</div>
              <div className="card-change positive">+2.4k today</div>
            </div>
          </div>

          <div className="overview-card">
            <div className="card-icon">◆</div>
            <div className="card-content">
              <div className="card-value">{stats.activeConnections}</div>
              <div className="card-label">Active Connections</div>
              <div className="card-change neutral">Real-time</div>
            </div>
          </div>

          <div className="overview-card">
            <div className="card-icon">⏱</div>
            <div className="card-content">
              <div className="card-value">{stats.avgResponseTime.toFixed(1)}ms</div>
              <div className="card-label">Avg Response Time</div>
              <div className="card-change positive">-5.2ms</div>
            </div>
          </div>
        </div>
      </section>

      <section className="database-operations">
        <h2>Database Operations</h2>

        <div className="operations-tabs">
          {[
            { id: 'overview', label: 'Overview', icon: '' },
            { id: 'citizens', label: 'Citizens', icon: '' },
            { id: 'energy', label: 'Energy', icon: '' },
            { id: 'queries', label: 'Query Builder', icon: '' }
          ].map(tab => (
            <button
              key={tab.id}
              className={`tab-button ${activeTab === tab.id ? 'active' : ''}`}
              onClick={() => setActiveTab(tab.id)}
            >
              <span className="tab-icon">{tab.icon}</span>
              {tab.label}
            </button>
          ))}
        </div>

        <div className="tab-content">
          {activeTab === 'overview' && (
            <DatabaseStatsPanel stats={stats} isConnected={isConnected} />
          )}

          {activeTab === 'citizens' && (
            <CitizenManager isConnected={isConnected} />
          )}

          {activeTab === 'energy' && (
            <EnergyMonitor isConnected={isConnected} />
          )}

          {activeTab === 'queries' && (
            <QueryBuilder isConnected={isConnected} />
          )}
        </div>
      </section>

      <section className="database-info">
        <h2>Database Architecture</h2>

        <div className="architecture-grid">
          <div className="arch-card">
            <h3>Database Models</h3>
            <div className="code-block">
              <pre><code>{`// Prisma Schema Example
model Citizen {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  district  District @relation(fields: [districtId], references: [id])
  districtId String
  energyUsage EnergyUsage[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model District {
  id       String    @id @default(cuid())
  name     String    @unique
  type     String
  citizens Citizen[]
  energyGrid EnergyGrid?
}`}</code></pre>
            </div>
          </div>

          <div className="arch-card">
            <h3>API Integration</h3>
            <div className="code-block">
              <pre><code>{`// Next.js API Route
export async function GET() {
  try {
    const citizens = await prisma.citizen.findMany({
      include: {
        district: true,
        energyUsage: {
          orderBy: { createdAt: 'desc' },
          take: 5
        }
      }
    });

    return Response.json(citizens);
  } catch (error) {
    return Response.json(
      { error: 'Database connection failed' },
      { status: 500 }
    );
  }
}`}</code></pre>
            </div>
          </div>

          <div className="arch-card">
            <h3>Real-time Updates</h3>
            <div className="code-block">
              <pre><code>{`// Server-Sent Events for real-time data
useEffect(() => {
  const eventSource = new EventSource('/api/events');

  eventSource.onmessage = (event) => {
    const data = JSON.parse(event.data);
    updateEnergyData(data);
  };

  return () => eventSource.close();
}, []);`}</code></pre>
            </div>
          </div>

          <div className="arch-card">
            <h3>Performance Optimization</h3>
            <div className="features-list">
              <div className="feature-item">
                <span className="feature-icon">▲</span>
                <span>Connection Pooling</span>
              </div>
              <div className="feature-item">
                <span className="feature-icon">◆</span>
                <span>Query Optimization</span>
              </div>
              <div className="feature-item">
                <span className="feature-icon">■</span>
                <span>Redis Caching</span>
              </div>
              <div className="feature-item">
                <span className="feature-icon">↻</span>
                <span>Background Jobs</span>
              </div>
            </div>
          </div>
        </div>
      </section>
    </div>
  );
}

Docs entries used in this lesson

Spotted a mistake in this lesson?

Useful articles