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
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
- Next.js Data Fetching Docs
- Prisma Documentation
- Supabase Guides
- MongoDB Atlas Documentation
- PostgreSQL Official Manual
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:
usersid, name, email, avatar, created_atpostsid, title, content, author_id, published_atcommentsid, 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
getServerSidePropsor 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.