How to Connect Nextjs With Database

How to Connect Next.js with a Database Next.js has rapidly become one of the most popular React frameworks for building modern web applications, thanks to its server-side rendering, static site generation, and seamless API route integration. However, one of the most critical aspects of any dynamic application is its ability to interact with a database. Whether you're building a blog, an e-commerce

Nov 10, 2025 - 08:40
Nov 10, 2025 - 08:40
 1

How to Connect Next.js with a Database

Next.js has rapidly become one of the most popular React frameworks for building modern web applications, thanks to its server-side rendering, static site generation, and seamless API route integration. However, one of the most critical aspects of any dynamic application is its ability to interact with a database. Whether you're building a blog, an e-commerce platform, or a SaaS product, connecting Next.js to a database enables you to store, retrieve, and manage data efficiently.

This comprehensive guide walks you through the entire process of connecting Next.js with a databasefrom choosing the right database and setting up the connection, to implementing secure and scalable data operations. Youll learn best practices, explore real-world examples, and discover tools that streamline development. By the end of this tutorial, youll be equipped to confidently integrate any database into your Next.js application, whether its PostgreSQL, MongoDB, MySQL, or a serverless option like Supabase or Firebase.

Step-by-Step Guide

Step 1: Choose the Right Database for Your Next.js Project

Before writing a single line of code, selecting the appropriate database is crucial. The choice depends on your applications data structure, scalability needs, and deployment environment.

Relational databases like PostgreSQL and MySQL are ideal for structured data with complex relationshipsthink user profiles, orders, and inventory systems. They enforce data integrity through schemas and support ACID transactions.

NoSQL databases like MongoDB are better suited for unstructured or semi-structured data, such as user-generated content, logs, or real-time analytics. They offer flexibility in schema design and scale horizontally with ease.

Serverless databases like Supabase (PostgreSQL-based), Firebase Firestore, or PlanetScale offer managed infrastructure, authentication integration, and real-time capabilitiesperfect for rapid prototyping and applications with variable traffic.

For this guide, well use PostgreSQL as our primary example due to its robustness, compatibility with Next.js, and widespread adoption in production environments. Well also briefly cover MongoDB and Supabase alternatives.

Step 2: Set Up Your Next.js Project

If you havent already created a Next.js project, start by initializing one using the official create-next-app tool:

npx create-next-app@latest my-next-app

cd my-next-app

npm run dev

This creates a fully configured Next.js project with TypeScript support, ESLint, and Tailwind CSS (optional). Navigate to http://localhost:3000 to confirm your app is running.

Step 3: Install Required Dependencies

To connect Next.js with PostgreSQL, well use the pg (Node.js PostgreSQL client) package. Install it via npm:

npm install pg

If you plan to use environment variables (which you absolutely should), ensure you have the .env.local file ready. Next.js automatically loads environment variables from this file.

For MongoDB users:

npm install mongodb

For Supabase users:

npm install @supabase/supabase-js

Step 4: Configure Database Connection Environment Variables

Never hardcode database credentials in your source code. Instead, store them securely in a .env.local file in your project root:

POSTGRES_URL=postgresql://username:password@localhost:5432/your_database_name

POSTGRES_HOST=localhost

POSTGRES_PORT=5432

POSTGRES_DB=your_database_name

POSTGRES_USER=username

POSTGRES_PASSWORD=password

For production, use environment variables provided by your hosting platform (Vercel, Netlify, etc.). On Vercel, you can set these under Project Settings > Environment Variables.

For MongoDB, your variables might look like:

MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/your_db?retryWrites=true&w=majority

For Supabase:

NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co

NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

Note: Prefix variables with NEXT_PUBLIC_ only if they need to be accessible in the browser. Database passwords and secret keys should never be prefixed this way.

Step 5: Create a Database Connection Module

Organize your database logic in a dedicated directory. Create a folder named lib (or db) at the root of your project:

mkdir lib/db

touch lib/db/postgres.js

In lib/db/postgres.js, create a reusable connection module:

import { Pool } from 'pg';

const pool = new Pool({

connectionString: process.env.POSTGRES_URL,

});

export default pool;

This uses pg.Pool to manage a pool of database connections, which is essential for performance under load. The pool automatically handles connection reuse, timeouts, and errors.

Important: Next.js runs both server and client code in the same bundle. To prevent database credentials from being exposed in the browser, always ensure database modules are imported only in server-side contexts (API routes, Server Components, or getServerSideProps).

Step 6: Create a Sample Database Table

Before querying data, ensure your database has a schema. Connect to your PostgreSQL instance using a client like psql, DBeaver, or pgAdmin, and run:

CREATE TABLE posts (

id SERIAL PRIMARY KEY,

title VARCHAR(255) NOT NULL,

content TEXT,

author VARCHAR(100),

created_at TIMESTAMP DEFAULT NOW()

);

Insert sample data:

INSERT INTO posts (title, content, author) VALUES

('My First Post', 'This is the content of my first blog post.', 'Alice'),

('Getting Started with Next.js', 'A guide to connecting Next.js with databases.', 'Bob');

Step 7: Query Data in Server Components (Next.js 13+)

With Next.js 13 and the App Router, server-side rendering is the default. Create a new page at app/posts/page.js:

'use client';

import PostList from '@/components/PostList';

export default function PostsPage() {

return <PostList />;

}

Now create the component components/PostList.js. Since this component will fetch data, it must be a Server Component:

'use server';

import pool from '@/lib/db/postgres';

export default async function PostList() {

const { rows } = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');

return (

<div>

<h2>Latest Posts</h2>

{rows.map(post => ( <div key={post.id} style={{ margin: '1rem 0', padding: '1rem', border: '1px solid

ddd' }}>

<h3>{post.title}</h3>

<p>{post.content}</p>

<small>By {post.author} on {new Date(post.created_at).toLocaleDateString()}</small>

</div>

))}

</div>

);

}

Notice the 'use server' directive. This explicitly marks the component as a Server Component, allowing direct database access without exposing credentials to the client.

Step 8: Create API Routes for Client-Side Data Fetching

If you prefer to fetch data from the client (e.g., using Reacts useEffect or SWR), create an API route. Create a file at app/api/posts/route.js:

import { NextResponse } from 'next/server';

import pool from '@/lib/db/postgres';

export async function GET() {

try {

const { rows } = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');

return NextResponse.json(rows);

} catch (error) {

console.error('Database error:', error);

return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 });

}

}

Now, from a client component, you can fetch this data:

'use client';

import { useEffect, useState } from 'react';

export default function ClientPostList() {

const [posts, setPosts] = useState([]);

useEffect(() => {

fetch('/api/posts')

.then(res => res.json())

.then(data => setPosts(data))

.catch(err => console.error('Error fetching posts:', err));

}, []);

return (

<div>

<h2>Posts from API Route</h2>

{posts.map(post => ( <div key={post.id} style={{ margin: '1rem 0', padding: '1rem', border: '1px solid

ddd' }}>

<h3>{post.title}</h3>

<p>{post.content}</p>

<small>By {post.author}</small>

</div>

))}

</div>

);

}

Using API routes decouples your frontend logic from your data layer, improves testability, and enables caching strategies like ISR (Incremental Static Regeneration).

Step 9: Insert Data into the Database

To create new posts, extend your API route to handle POST requests:

import { NextRequest, NextResponse } from 'next/server';

import pool from '@/lib/db/postgres';

export async function GET() {

const { rows } = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');

return NextResponse.json(rows);

}

export async function POST(request: NextRequest) {

const body = await request.json();

const { title, content, author } = body;

if (!title || !author) {

return NextResponse.json({ error: 'Title and author are required' }, { status: 400 });

}

try {

const result = await pool.query(

'INSERT INTO posts (title, content, author) VALUES ($1, $2, $3) RETURNING *',

[title, content || '', author]

);

return NextResponse.json(result.rows[0], { status: 201 });

} catch (error) {

console.error('Insert error:', error);

return NextResponse.json({ error: 'Failed to create post' }, { status: 500 });

}

}

Now, from a client form, you can submit data:

'use client';

import { useState } from 'react';

export default function CreatePostForm() {

const [title, setTitle] = useState('');

const [content, setContent] = useState('');

const [author, setAuthor] = useState('');

const [message, setMessage] = useState('');

const handleSubmit = async (e: React.FormEvent) => {

e.preventDefault();

const res = await fetch('/api/posts', {

method: 'POST',

headers: { 'Content-Type': 'application/json' },

body: JSON.stringify({ title, content, author }),

});

const data = await res.json();

if (res.ok) {

setMessage('Post created successfully!');

setTitle('');

setContent('');

setAuthor('');

} else {

setMessage(data.error || 'Failed to create post');

}

};

return ( <form onSubmit={handleSubmit} style={{ margin: '2rem 0', padding: '1rem', border: '1px solid

eee' }}>

<h3>Create a New Post</h3>

<input

type="text"

placeholder="Title"

value={title}

onChange={(e) => setTitle(e.target.value)}

required

style={{ width: '100%', padding: '0.5rem', margin: '0.5rem 0' }}

/>

<input

type="text"

placeholder="Author"

value={author}

onChange={(e) => setAuthor(e.target.value)}

required

style={{ width: '100%', padding: '0.5rem', margin: '0.5rem 0' }}

/>

<textarea

placeholder="Content"

value={content}

onChange={(e) => setContent(e.target.value)}

style={{ width: '100%', padding: '0.5rem', margin: '0.5rem 0', minHeight: '100px' }}

/> <button type="submit" style={{ padding: '0.5rem 1rem', background: '

0070f3', color: 'white', border: 'none', cursor: 'pointer' }}>Create Post</button>

{message && <p style={{ marginTop: '1rem', color: '

d9534f' }}>{message}</p>}

</form>

);

}

Step 10: Connect to MongoDB (Alternative)

If you prefer MongoDB, update your lib/db/mongodb.js:

import { MongoClient } from 'mongodb';

if (!process.env.MONGODB_URI) {

throw new Error('Invalid/Missing environment variable: "MONGODB_URI"');

}

const uri = process.env.MONGODB_URI;

const options = {

useNewUrlParser: true,

useUnifiedTopology: true,

};

let client;

let clientPromise: Promise<MongoClient>;

if (process.env.NODE_ENV === 'development') {

// In development mode, use a global variable to preserve the client across hot reloads

if (!global._mongoClientPromise) {

client = new MongoClient(uri, options);

global._mongoClientPromise = client.connect();

}

clientPromise = global._mongoClientPromise;

} else {

// In production mode, create a new client each time

client = new MongoClient(uri, options);

clientPromise = client.connect();

}

export default clientPromise;

Then, in your Server Component or API route:

import clientPromise from '@/lib/db/mongodb';

export default async function PostList() {

const client = await clientPromise;

const db = client.db('your_database_name');

const posts = await db.collection('posts').find({}).toArray();

return (

<div>

{posts.map(post => ( <div key={post._id} style={{ margin: '1rem 0', padding: '1rem', border: '1px solid

ddd' }}>

<h3>{post.title}</h3>

<p>{post.content}</p>

<small>By {post.author}</small>

</div>

))}

</div>

);

}

Step 11: Use Supabase (Serverless Alternative)

Supabase offers a PostgreSQL database with built-in authentication and real-time subscriptions. Install the client:

npm install @supabase/supabase-js

Create lib/supabase/client.js:

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';

const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

Use it in a Server Component:

'use server';

import { supabase } from '@/lib/supabase/client';

export default async function PostList() {

const { data: posts, error } = await supabase

.from('posts')

.select('*')

.order('created_at', { ascending: false });

if (error) {

console.error('Supabase error:', error);

return <p>Failed to load posts.</p>;

}

return (

<div>

{posts.map(post => ( <div key={post.id} style={{ margin: '1rem 0', padding: '1rem', border: '1px solid

ddd' }}>

<h3>{post.title}</h3>

<p>{post.content}</p>

<small>By {post.author}</small>

</div>

))}

</div>

);

}

Supabase also allows you to generate RESTful APIs automatically from your database tablesgreat for rapid development.

Best Practices

Use Environment Variables for All Credentials

Hardcoding secrets in source code is a severe security risk. Always use .env.local for local development and environment variables on your hosting platform. Never commit .env.local to version controladd it to your .gitignore.

Implement Connection Pooling

Opening a new database connection for every request is inefficient and can lead to connection exhaustion. Always use a connection pool (e.g., pg.Pool for PostgreSQL). This reuses existing connections and improves performance under concurrent traffic.

Prevent SQL Injection with Parameterized Queries

Never concatenate user input directly into SQL strings. Always use parameterized queries with placeholders:

// ? DON'T DO THIS

const query = SELECT * FROM posts WHERE author = '${userInput}';

// ? DO THIS INSTEAD

const { rows } = await pool.query(

'SELECT * FROM posts WHERE author = $1',

[userInput]

);

The pg library automatically escapes values, preventing injection attacks.

Separate Concerns: Keep Database Logic Outside Components

While its tempting to write database queries directly in Server Components, its better to abstract them into dedicated service modules:

// lib/services/postService.js

import pool from '@/lib/db/postgres';

export async function getAllPosts() {

const { rows } = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');

return rows;

}

export async function createPost(title, content, author) {

const result = await pool.query(

'INSERT INTO posts (title, content, author) VALUES ($1, $2, $3) RETURNING *',

[title, content, author]

);

return result.rows[0];

}

Then import and use these functions in your components or API routes. This improves testability, reusability, and maintainability.

Use TypeScript for Type Safety

Define interfaces for your data models:

export interface Post {

id: number;

title: string;

content: string | null;

author: string;

created_at: string;

}

Then use it in your service functions:

export async function getAllPosts(): Promise<Post[]> {

const { rows } = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');

return rows as Post[];

}

TypeScript helps catch errors early and improves developer experience with autocomplete and documentation.

Handle Errors Gracefully

Always wrap database operations in try-catch blocks and return meaningful error responses. Avoid exposing internal stack traces to users. Log errors for debugging but show user-friendly messages.

Enable Connection Health Checks

For production applications, implement health checks to detect and recover from failed database connections. Libraries like pg-pool have built-in retry mechanisms, but consider adding a liveness endpoint in your API:

export async function GET() {

try {

await pool.query('SELECT 1');

return NextResponse.json({ status: 'ok' });

} catch (error) {

console.error('Database health check failed:', error);

return NextResponse.json({ status: 'error' }, { status: 503 });

}

}

Use Prisma or Drizzle for Advanced Use Cases

For complex applications, consider using an ORM (Object-Relational Mapper) like Prisma or Drizzle ORM. They provide type-safe query builders, migrations, and schema management:

npm install prisma @prisma/client

npx prisma init

Prisma generates a client based on your schema, reducing boilerplate and preventing SQL errors. Its especially useful for teams scaling beyond simple CRUD operations.

Tools and Resources

Database Tools

  • PostgreSQL Open-source, enterprise-grade relational database. Ideal for structured data.
  • MongoDB Atlas Fully managed MongoDB service with free tier and global clustering.
  • Supabase Open-source Firebase alternative with PostgreSQL, auth, and real-time features.
  • Firebase Firestore NoSQL document database with real-time sync, great for mobile and web apps.
  • PlanetScale Serverless MySQL with branching and instant scaling.
  • DBeaver Free, cross-platform database tool for querying and managing SQL databases.
  • pgAdmin Web-based administration tool for PostgreSQL.

ORMs and Query Builders

  • Prisma Modern ORM with schema-first approach and auto-generated clients.
  • Drizzle ORM Type-safe SQL builder with zero runtime overhead. Works well with Next.js.
  • Knex.js SQL query builder for PostgreSQL, MySQL, SQLite, and more.
  • Mongoose ODM (Object Document Mapper) for MongoDB with schema validation.

Development & Debugging

  • Next.js Dev Tools Built-in hot reloading and error overlay.
  • React DevTools Inspect component state and props.
  • Postman / Insomnia Test your API routes manually.
  • SWR React hooks for data fetching with caching and revalidation.
  • React Query Advanced data fetching and caching library.

Deployment Platforms

  • Vercel Official Next.js host with automatic environment variable injection.
  • Netlify Supports Next.js with serverless functions and edge functions.
  • Render Simple hosting for databases and Node.js apps.
  • Fly.io Deploy globally with low-latency edge routing.

Learning Resources

Real Examples

Example 1: Blog with Next.js and PostgreSQL

Imagine youre building a personal blog with articles, comments, and user profiles. Youd have tables like:

  • users id, name, email, avatar, created_at
  • posts id, title, content, author_id, published_at
  • comments id, post_id, user_id, text, created_at

Youd use Server Components to render blog posts with comments, API routes to handle form submissions, and Prisma for type-safe queries. Youd also implement pagination and caching using ISR to improve performance.

Example 2: E-commerce Product Catalog

An online store needs to display products, filter by category, and manage inventory. With Next.js and PostgreSQL:

  • Use getServerSideProps or Server Components to fetch products with filters.
  • Implement search using PostgreSQLs full-text search: to_tsvector('english', title) @@ to_tsquery('english', $1)
  • Use API routes to handle cart updates and checkout.
  • Store order history in a relational schema for reporting.

Example 3: Real-Time Dashboard with Supabase

Supabase enables real-time updates via its on() subscription method:

import { useEffect } from 'react';

import { supabase } from '@/lib/supabase/client';

export default function RealTimeDashboard() {

const [stats, setStats] = useState([]);

useEffect(() => {

const channel = supabase

.channel('stats')

.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'analytics' }, (payload) => {

setStats(prev => [...prev, payload.new]);

})

.subscribe();

return () => {

supabase.removeChannel(channel);

};

}, []);

return (

<div>

{stats.map(stat => (

<p key={stat.id}>New event: {stat.event} at {stat.timestamp}</p>

))}

</div>

);

}

This creates a live dashboard that updates without page refreshesperfect for analytics, chat apps, or live notifications.

Example 4: User Authentication with NextAuth and PostgreSQL

Combine Next.js with NextAuth and PostgreSQL to handle user sign-up, login, and sessions:

// lib/auth/[...nextauth].js

import NextAuth from 'next-auth';

import PostgreSQLAdapter from '@auth/pg-adapter';

import { pool } from '@/lib/db/postgres';

export const authOptions = {

adapter: PostgreSQLAdapter(pool),

providers: [

// Add your providers (Google, GitHub, etc.)

],

};

export default NextAuth(authOptions);

NextAuth automatically creates tables like accounts, sessions, and users in your PostgreSQL database, eliminating manual schema management.

FAQs

Can I connect Next.js to a database on the client side?

No, you should never connect directly to a database from the client side (browser). This exposes your credentials and opens your system to attacks. Always use API routes or Server Components to mediate database access.

Which database is best for Next.js?

PostgreSQL is the most popular and recommended choice due to its reliability, performance, and compatibility with Next.js. Supabase is ideal for developers who want a managed, serverless experience. MongoDB is suitable for flexible, document-based data. Choose based on your data structure and scaling needs.

Do I need an ORM for Next.js?

No, you dont need an ORM for simple applications. Direct SQL queries with pg or mongodb work fine. However, ORMs like Prisma or Drizzle are highly recommended for larger applications, teams, or when you need type safety, migrations, and complex relationships.

How do I handle database migrations in Next.js?

Use migration tools like Prisma Migrate, Knex.js migrations, or raw SQL scripts. Store migration files in a migrations/ folder and run them manually or via CI/CD. Never rely on automatic schema generation in production.

How do I secure my database connection in production?

Use environment variables, restrict database user permissions (dont use superuser accounts), enable SSL connections, and use connection pooling. For cloud databases like Supabase or MongoDB Atlas, enable IP whitelisting and use strong passwords or API keys.

Can I use SQLite with Next.js?

Technically yes, but its not recommended for production web apps. SQLite is file-based and doesnt support concurrent writes well. Its suitable only for local development or static sites with minimal data.

How do I test database queries in Next.js?

Use Jest or Vitest to write unit tests for your service modules. Mock the database connection using libraries like mock-fs or pg-mock. For integration tests, use a test database and run migrations before each test suite.

Whats the difference between Server Components and API routes for database access?

Server Components render data on the server and send HTML to the clientideal for initial page loads. API routes expose endpoints that can be consumed by clients (React components, mobile apps, third-party services). Use Server Components for page data and API routes for dynamic or client-initiated actions.

Conclusion

Connecting Next.js with a database is a foundational skill for building dynamic, data-driven web applications. Whether you choose PostgreSQL, MongoDB, or a serverless solution like Supabase, the principles remain the same: prioritize security, organize your code logically, and leverage Next.jss server-side rendering capabilities to deliver fast, scalable experiences.

In this guide, youve learned how to set up database connections, write secure queries, structure your project for maintainability, and apply best practices for production deployment. Youve seen real-world examples ranging from blogs to real-time dashboards and explored tools that streamline development.

As you continue building, remember that the goal isnt just to connect to a databaseits to build applications that are fast, secure, and easy to maintain. Start simple, iterate often, and always prioritize clean architecture over quick hacks.

With the knowledge youve gained here, youre now equipped to confidently integrate databases into any Next.js projectwhether its a personal portfolio or a high-traffic SaaS platform. The next step? Build something amazing.