How to Connect Nextjs With Database
How to Connect Next.js With a Database Next.js has rapidly become the go-to framework for building modern, high-performance React applications. Its hybrid rendering model—supporting Server-Side Rendering (SSR), Static Site Generation (SSG), and Client-Side Rendering (CSR)—makes it ideal for content-heavy websites, e-commerce platforms, dashboards, and API-driven applications. However, one of the m
How to Connect Next.js With a Database
Next.js has rapidly become the go-to framework for building modern, high-performance React applications. Its hybrid rendering modelsupporting Server-Side Rendering (SSR), Static Site Generation (SSG), and Client-Side Rendering (CSR)makes it ideal for content-heavy websites, e-commerce platforms, dashboards, and API-driven applications. However, one of the most common challenges developers face when adopting Next.js is connecting it to a database. Unlike traditional full-stack frameworks, Next.js abstracts server and client logic, requiring a deliberate approach to database integration.
Connecting Next.js with a database is not just about executing queriesits about understanding where to place database logic, how to manage connections efficiently, how to secure sensitive data, and how to optimize performance across rendering modes. This guide provides a comprehensive, step-by-step walkthrough of how to connect Next.js with various databases, including PostgreSQL, MongoDB, and SQLite, while adhering to industry best practices. Whether you're building a personal blog, a SaaS product, or an enterprise application, mastering database connectivity in Next.js is essential for scalability, security, and maintainability.
Step-by-Step Guide
1. Choose Your Database
Before writing a single line of code, select the right database for your use case. The choice impacts your architecture, performance, and developer experience.
- PostgreSQL: Best for structured data, complex queries, and applications requiring ACID compliance. Ideal for financial systems, e-commerce, and content management.
- MongoDB: A NoSQL document database perfect for flexible schemas, rapid prototyping, and applications with unstructured or semi-structured data like user-generated content.
- SQLite: Lightweight, file-based, and zero-configuration. Great for small-scale apps, local development, or embedded systems.
- MySQL: Similar to PostgreSQL but with slightly simpler syntax. Widely used in legacy systems and shared hosting environments.
- Supabase / Firebase: Backend-as-a-Service (BaaS) options that abstract database management and provide real-time capabilities.
For this guide, well focus on PostgreSQL and MongoDB, as they represent the two most common paradigms: relational and document-based databases.
2. Set Up Your Next.js Project
If you havent already created a Next.js project, initialize one using the official CLI:
npx create-next-app@latest my-next-app
cd my-next-app
Choose default options unless you have specific requirements. Once created, install the necessary database drivers:
For PostgreSQL:
npm install pg
For MongoDB:
npm install mongodb
Next.js projects use a modular structure. We recommend organizing your database logic under a dedicated folder:
src/
??? lib/
? ??? db/
? ? ??? postgres.js
? ? ??? mongodb.js
? ??? utils/
3. Connect to PostgreSQL
PostgreSQL is a powerful, open-source relational database. To connect Next.js to PostgreSQL, we use the pg library, which provides a robust client for Node.js.
Create the file src/lib/db/postgres.js:
import { Pool } from 'pg';
const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 5432,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
});
export default pool;
Next, create a .env.local file in your project root to store your database credentials:
DB_USER=your_username
DB_HOST=localhost
DB_NAME=your_database
DB_PASSWORD=your_password
DB_PORT=5432
?? Never commit your .env.local file to version control. Add it to your .gitignore.
To test the connection, create a simple API route in app/api/test-postgres/route.js (for App Router) or pages/api/test-postgres.js (for Pages Router):
App Router Example:
import { NextResponse } from 'next/server';
import pool from '@/lib/db/postgres';
export async function GET() {
try {
const res = await pool.query('SELECT NOW()');
return NextResponse.json({ currentTime: res.rows[0].now });
} catch (error) {
console.error('Database connection error:', error);
return NextResponse.json({ error: 'Failed to connect to database' }, { status: 500 });
}
}
Visit http://localhost:3000/api/test-postgres to verify the connection returns the current timestamp.
4. Connect to MongoDB
MongoDB stores data in flexible, JSON-like documents. To connect Next.js to MongoDB, use the official mongodb driver.
Create src/lib/db/mongodb.js:
import { MongoClient } from 'mongodb';
if (!process.env.MONGODB_URI) {
throw new Error('Please add your Mongo URI to .env.local');
}
const uri = process.env.MONGODB_URI;
const options = {
useNewUrlParser: true,
useUnifiedTopology: true,
};
let client;
let clientPromise;
if (process.env.NODE_ENV === 'development') {
// In development mode, use a global variable so we don't create multiple instances
if (!global._mongoClientPromise) {
client = new MongoClient(uri, options);
global._mongoClientPromise = client.connect();
}
clientPromise = global._mongoClientPromise;
} else {
// In production mode, it's best to not use globals
client = new MongoClient(uri, options);
clientPromise = client.connect();
}
export default clientPromise;
Update your .env.local with the MongoDB connection string:
MONGODB_URI=mongodb+srv://username:password@cluster0.xxxxx.mongodb.net/your_database?retryWrites=true&w=majority
Now create an API route to test the connection in app/api/test-mongodb/route.js:
import { NextResponse } from 'next/server';
import clientPromise from '@/lib/db/mongodb';
export async function GET() {
try {
const client = await clientPromise;
const db = client.db();
const collections = await db.listCollections().toArray();
return NextResponse.json({ collections: collections.map(c => c.name) });
} catch (error) {
console.error('MongoDB connection error:', error);
return NextResponse.json({ error: 'Failed to connect to MongoDB' }, { status: 500 });
}
}
Visit the endpoint to confirm MongoDB connectivity.
5. Create a Database Schema and Table
Once connected, define your data model. For PostgreSQL, create a table to store blog posts:
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
author VARCHAR(100),
published BOOLEAN DEFAULT false,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
For MongoDB, create a collection named posts with a schema like this:
{
"title": "How to Connect Next.js with a Database",
"content": "This is the article content...",
"author": "John Doe",
"published": true,
"createdAt": ISODate("2024-06-10T10:00:00Z")
}
6. Build CRUD Operations
Now implement Create, Read, Update, and Delete operations. Well use the App Router structure.
Creating a Post (PostgreSQL)
// app/api/posts/route.js
import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db/postgres';
export async function POST(request: NextRequest) {
const { title, content, author } = await request.json();
if (!title || !content || !author) {
return NextResponse.json({ error: 'Title, content, and author are required' }, { status: 400 });
}
try {
const res = await pool.query(
'INSERT INTO posts (title, content, author) VALUES ($1, $2, $3) RETURNING *',
[title, content, author]
);
return NextResponse.json(res.rows[0], { status: 201 });
} catch (error) {
console.error('Error creating post:', error);
return NextResponse.json({ error: 'Failed to create post' }, { status: 500 });
}
}
Reading All Posts (PostgreSQL)
export async function GET() {
try {
const res = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');
return NextResponse.json(res.rows);
} catch (error) {
console.error('Error fetching posts:', error);
return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 });
}
}
Updating a Post
export async function PUT(request: NextRequest) {
const { id, title, content, author } = await request.json();
if (!id) {
return NextResponse.json({ error: 'Post ID is required' }, { status: 400 });
}
try {
const res = await pool.query(
'UPDATE posts SET title = $1, content = $2, author = $3 WHERE id = $4 RETURNING *',
[title, content, author, id]
);
if (res.rowCount === 0) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
return NextResponse.json(res.rows[0]);
} catch (error) {
console.error('Error updating post:', error);
return NextResponse.json({ error: 'Failed to update post' }, { status: 500 });
}
}
Deleting a Post
export async function DELETE(request: NextRequest) {
const { id } = await request.json();
if (!id) {
return NextResponse.json({ error: 'Post ID is required' }, { status: 400 });
}
try {
const res = await pool.query('DELETE FROM posts WHERE id = $1 RETURNING id', [id]);
if (res.rowCount === 0) {
return NextResponse.json({ error: 'Post not found' }, { status: 404 });
}
return NextResponse.json({ message: 'Post deleted successfully' });
} catch (error) {
console.error('Error deleting post:', error);
return NextResponse.json({ error: 'Failed to delete post' }, { status: 500 });
}
}
For MongoDB, the syntax is similar but uses insertOne, find, updateOne, and deleteOne methods.
7. Use Database Queries in Server Components
Next.js App Router allows you to fetch data directly in Server Components using async/await. This is ideal for SSR.
Create a server component to display blog posts:
// app/posts/page.tsx
import Link from 'next/link';
import pool from '@/lib/db/postgres';
export default async function PostsPage() {
const res = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');
const posts = res.rows;
return (
<div>
<h1>All Posts</h1>
<Link href="/posts/new">Create New Post</Link>
<ul>
{posts.map(post => (
<li key={post.id}>
<h2><Link href={/posts/${post.id}}>{post.title}</Link></h2>
<p>By {post.author} on {new Date(post.created_at).toLocaleDateString()}</p>
</li>
))}
</ul>
</div>
);
}
Next.js automatically handles the server-side execution. The database query runs on the server, and the rendered HTML is sent to the clientimproving SEO and initial load performance.
8. Use Database Queries in Server Actions (Next.js 13.4+)
Server Actions provide a cleaner way to handle form submissions and mutations without creating API routes.
Create a server action in app/posts/actions.ts:
'use server';
import pool from '@/lib/db/postgres';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
const author = formData.get('author') as string;
const res = await pool.query(
'INSERT INTO posts (title, content, author) VALUES ($1, $2, $3) RETURNING *',
[title, content, author]
);
return res.rows[0];
}
Use it in a form component:
// app/posts/new/page.tsx
'use client';
import { createPost } from '@/app/posts/actions';
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" placeholder="Title" required />
<textarea name="content" placeholder="Content" required />
<input name="author" placeholder="Author" required />
<button type="submit">Create Post</button>
</form>
);
}
Server Actions eliminate the need for separate API routes for mutations and provide built-in form handling and validation.
Best Practices
1. Use Environment Variables for Secrets
Never hardcode database credentials. Always use .env.local and load them via process.env. Next.js automatically loads these variables at build time for server-side code.
For production, use platform-specific secrets (e.g., Vercels Environment Variables, Netlifys Site Settings, or Docker secrets).
2. Implement Connection Pooling
Opening a new database connection for every request is inefficient and can exhaust available connections. Use connection pooling.
Both pg (PostgreSQL) and mongodb drivers support pooling by default. Configure pool size based on your apps traffic:
const pool = new Pool({
max: 20, // maximum number of clients in the pool
idleTimeoutMillis: 30000, // close idle clients after 30 seconds
connectionTimeoutMillis: 2000, // return an error after 2 seconds if connection could not be established
});
3. Avoid Database Calls in Client Components
Client components run in the browser and should never directly connect to databases. Exposing credentials or database endpoints to the client is a severe security risk.
Always use API routes or Server Actions to mediate database access. Even if you use a BaaS like Supabase, always use server-side authentication tokens, not client-side API keys.
4. Use Prepared Statements to Prevent SQL Injection
Always use parameterized queries instead of string concatenation. The pg library automatically escapes values when using $1, $2 placeholders.
? Dangerous:
const query = SELECT * FROM users WHERE email = '${email}';
? Safe:
const query = 'SELECT * FROM users WHERE email = $1';
const res = await pool.query(query, [email]);
5. Handle Errors Gracefully
Database connections can fail due to network issues, timeouts, or authentication errors. Always wrap queries in try-catch blocks and return appropriate HTTP status codes.
Log errors for debugging but avoid exposing sensitive stack traces to clients.
6. Optimize Queries and Use Indexes
As your data grows, unoptimized queries become bottlenecks. Use EXPLAIN ANALYZE in PostgreSQL to inspect query performance.
Create indexes on frequently queried columns:
CREATE INDEX idx_posts_author ON posts(author);
CREATE INDEX idx_posts_published ON posts(published);
7. Use TypeScript for Type Safety
Define TypeScript interfaces for your database models to catch errors early:
interface Post {
id: number;
title: string;
content: string;
author: string;
published: boolean;
created_at: Date;
}
Use this interface to type your API responses and server component props.
8. Implement Caching
For read-heavy applications, cache frequently accessed data using Redis or Next.jss built-in revalidation features.
Use revalidatePath or revalidateTag to invalidate cached pages after mutations:
import { revalidatePath } from 'next/cache';
export async function POST() {
// ... insert post
revalidatePath('/posts');
return NextResponse.json({ success: true });
}
9. Separate Concerns with a Data Access Layer
Organize database logic into a dedicated module to keep API routes and server components clean:
// src/lib/db/repository/postRepository.ts
import pool from '@/lib/db/postgres';
export const getAllPosts = async () => {
const res = await pool.query('SELECT * FROM posts ORDER BY created_at DESC');
return res.rows;
};
export const getPostById = async (id: number) => {
const res = await pool.query('SELECT * FROM posts WHERE id = $1', [id]);
return res.rows[0];
};
export const createPost = async (title: string, content: string, author: string) => {
const res = await pool.query(
'INSERT INTO posts (title, content, author) VALUES ($1, $2, $3) RETURNING *',
[title, content, author]
);
return res.rows[0];
};
Then import and use these functions in your API routes and server components.
Tools and Resources
Database Tools
- pgAdmin: Open-source administration and development platform for PostgreSQL.
- MongoDB Compass: GUI for exploring and managing MongoDB databases.
- Supabase: Open-source Firebase alternative with PostgreSQL backend, real-time subscriptions, and authentication.
- PlanetScale: Serverless MySQL database with branching and schema migrations.
- Neon.tech: Serverless PostgreSQL with separation of compute and storage.
ORMs and Query Builders
While raw SQL is powerful, ORMs can accelerate development and improve maintainability.
- Prisma: Modern ORM with auto-generated types, migrations, and a powerful query builder. Highly recommended for Next.js.
- Drizzle ORM: Lightweight, type-safe ORM with excellent TypeScript support and zero runtime overhead.
- Knex.js: SQL query builder with a fluent API, ideal for complex queries.
- Mongoose: MongoDB ODM (Object Document Mapper) with schema validation and middleware.
For most Next.js applications, we recommend Prisma due to its seamless TypeScript integration and automatic type generation from your schema.
Environment Management
- Dotenv: Loads environment variables from
.envfiles. - Vercel Environment Variables: Securely manage secrets in production.
- 12-Factor App Methodology: Follow best practices for configuration management.
Monitoring and Logging
- LogRocket: Session replay and error tracking.
- Sentry: Real-time error monitoring with performance insights.
- PostHog: Open-source product analytics with database query tracking.
Learning Resources
- Next.js Data Fetching Documentation
- Prisma Client Docs
- PostgreSQL Official Documentation
- MongoDB Manual
- Next.js + PostgreSQL Full Tutorial (YouTube)
Real Examples
Example 1: Blog with Next.js and PostgreSQL
A content creator builds a personal blog using Next.js, PostgreSQL, and Prisma. The blog supports:
- Server-side rendered homepage listing all published posts
- Dynamic routes for individual posts (
/posts/[id]) - Admin dashboard with form to create/edit posts using Server Actions
- Comment section with real-time updates via WebSockets (using Pusher)
Prisma schema:
// prisma/schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String
author String
published Boolean @default(false)
createdAt DateTime @default(now())
}
Generated TypeScript types ensure type safety across the entire stackfrom database to frontend.
Example 2: E-commerce Product Catalog with MongoDB
An online store uses Next.js and MongoDB to manage a catalog of products with varying attributes (e.g., clothing sizes, electronics specs).
Each product document looks like:
{
"name": "Wireless Headphones",
"category": "Electronics",
"price": 199.99,
"specs": {
"batteryLife": "20 hours",
"connectivity": "Bluetooth 5.0"
},
"tags": ["wireless", "audio", "premium"],
"inStock": true,
"createdAt": ISODate("2024-01-15T08:00:00Z")
}
Next.js fetches products via API routes, filters them using query parameters (?category=Electronics&minPrice=100), and caches results using revalidateTag to ensure fast load times during high traffic.
Example 3: Internal Dashboard with SQLite
A small team builds a lightweight internal tool to track project tasks. Since the app is used locally and data volume is low, they use SQLite for simplicity.
The app is bundled as a desktop application using Electron, and SQLite files are stored locally. Next.js handles the UI, while the SQLite database runs on the same machine.
This example demonstrates that database choice should align with deployment contextnot just scale.
FAQs
Can I connect Next.js directly to a database from a client component?
No. Client components run in the browser and cannot safely access database credentials. Always use API routes or Server Actions as intermediaries to protect your database from exposure.
Which is better: Prisma or raw SQL in Next.js?
For small projects or teams familiar with SQL, raw queries are fine. For larger applications requiring type safety, migrations, and developer productivity, Prisma is superior. It reduces boilerplate, prevents SQL injection, and generates TypeScript types automatically.
How do I handle database migrations in Next.js?
Use tools like Prisma Migrate, Knex.js migrations, or manual SQL scripts. Never modify your database schema manually in production. Always version-control your migrations and apply them via CI/CD pipelines.
Is it okay to use MongoDB with Next.js for production?
Absolutely. Many production applications use MongoDB with Next.js successfully. Ensure you use connection pooling, secure your MongoDB Atlas cluster with IP whitelisting and strong credentials, and avoid exposing the connection string publicly.
How do I connect to a remote database in production?
Use environment variables to store the remote database URL. In Vercel or Netlify, add these variables in your project settings. Never hardcode them. For PostgreSQL, ensure your provider (e.g., Supabase, Neon, AWS RDS) allows connections from your apps IP address.
Should I use Next.js API routes or Server Actions for database queries?
Use Server Actions for mutations (POST, PUT, DELETE) triggered by forms. Use API routes for complex queries, authentication endpoints, or when you need to expose a public API. Server Actions are simpler and more integrated with Reacts data flow.
How do I secure my database credentials in Next.js?
Store credentials in .env.local and never commit it. Use platform-specific secrets (Vercel, Netlify, etc.). For serverless functions, avoid using long-lived credentialsprefer short-lived tokens or OAuth where possible.
Can I use Next.js with Firebase Firestore?
Yes. Firebase Firestore is a NoSQL document database. Install the Firebase SDK and initialize it in a server action or API route. However, avoid initializing it in client components unless youre using Firebase Auth with secure rules.
Why is my database connection slow in Next.js?
Possible causes: lack of connection pooling, unindexed queries, network latency, or cold starts on serverless platforms. Use connection pooling, optimize queries, and consider using a dedicated database host (not free-tier) for production.
Do I need to close database connections in Next.js?
With connection pooling, you dont need to manually close connections. The pool manages them automatically. However, if youre using a singleton client (like MongoDB), ensure its initialized once and reused across requests.
Conclusion
Connecting Next.js with a database is a foundational skill for building dynamic, data-driven applications. Whether you choose PostgreSQL for its reliability, MongoDB for its flexibility, or SQLite for simplicity, the key is to implement the connection securely, efficiently, and in alignment with Next.jss rendering model.
By following the practices outlined in this guideusing environment variables, connection pooling, server-side queries, and proper error handlingyou ensure your application is not only functional but also scalable, secure, and maintainable. Avoid client-side database access at all costs. Leverage Server Components and Server Actions to keep your logic server-bound and your data protected.
As you progress, consider adopting Prisma or Drizzle ORM to streamline development and eliminate manual typing. Integrate caching and monitoring tools to optimize performance and detect issues early. And always test your database connections in staging environments that mirror production.
Next.js is powerful because it gives you control over where and how your data flows. Use that control wisely. The right database connection strategy can transform a static site into a robust, real-time application capable of handling thousands of users with ease.
Start small, test thoroughly, and iterate. Your usersand your future selfwill thank you.