
By Peter, backend engineer at Unifa.
As someone who mainly works with Ruby on Rails and Vue.js in my day-to-day projects, I always enjoy exploring new frontend technologies in my spare time. Recently, I decided to give Next.js a try — not just to see what it’s like, but also to connect it to a local database and fetch some data, just to get a feel for how the whole flow works in a Next.js setup.
Next.js is great because it:
- Supports hybrid rendering — you can pre-render pages on the server (SSR/SSG) but still do dynamic things on the client
- Simplifies navigation — just drop your page in the pages/ folder and it works
- Makes API integration easy — write server functions as part of the frontend project
- Helps you scale — with tools like Backend for Frontend (BFF)
Let’s Build a Modal in Next.js
Here’s a simple but powerful example: showing post content in a modal when users click a title.
Step 1: Create Your Next.js Project
npx create-next-app@latest my-next-modal
cd my-next-modal
npm install react-modal prisma @prisma/client
npx prisma init
Step 2: Set Up a Local Database with Prisma
Replace the contents of prisma/schema.prisma with:
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
body String
}
npx prisma migrate dev --name init
This will generate a local SQLite database (dev.db) with a Post table. You can populate it using Prisma Studio:
npx prisma studio
Step 3: Configure Modal for Accessibility
Inside pages/_app.js:
// pages/_app.js import { useEffect } from 'react' import Modal from 'react-modal' import '../styles/globals.css' Modal.setAppElement('#__next') // Ensure modal accessibility function MyApp({ Component, pageProps }) { return <Component {...pageProps} /> } export default MyApp
This ensures screen readers won’t read the background while the modal is open.
Step 4: Add API Routes for Fetching Posts
Create the following files:
pages/api/posts.js
import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() export default async function handler(req, res) { const posts = await prisma.post.findMany() res.status(200).json(posts) }
pages/api/posts/[id].js
import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() export default async function handler(req, res) { const { id } = req.query const post = await prisma.post.findUnique({ where: { id: parseInt(id) }, }) if (post) { res.status(200).json(post) } else { res.status(404).json({ error: 'Post not found' }) } }
Step 5: Build the List + Modal Component
Replace pages/index.js with:
import { useState } from 'react' import Modal from 'react-modal' export default function Home({ posts }) { const [isOpen, setIsOpen] = useState(false) const [activePost, setActivePost] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) async function openModal(id) { setIsOpen(true) setLoading(true) setError(null) try { const res = await fetch(`/api/posts/${id}`) if (!res.ok) throw new Error('Failed to fetch') const post = await res.json() setActivePost(post) } catch (e) { setError(e.message) } finally { setLoading(false) } } function closeModal() { setIsOpen(false) setActivePost(null) } return ( <div style={{ padding: 20 }}> <h1>Posts</h1> <ul> {posts.map((p) => ( <li key={p.id} style={{ marginBottom: 8 }}> <button onClick={() => openModal(p.id)} style={{ background: 'none', border: 'none', color: '#0070f3', cursor: 'pointer', padding: 0, }} > {p.title} </button> </li> ))} </ul> <Modal isOpen={isOpen} onRequestClose={closeModal} style={{ overlay: { backgroundColor: 'rgba(0,0,0,0.5)' }, content: { inset: '20% 10%', padding: 20 }, }} > <button onClick={closeModal} style={{ float: 'right' }}>Close</button> {loading && <p>Loading…</p>} {error && <p style={{ color: 'red' }}>Error: {error}</p>} {activePost && ( <> <h2>{activePost.title}</h2> <p>{activePost.body}</p> </> )} </Modal> </div> ) } export async function getStaticProps() { const res = await fetch('http://localhost:3000/api/posts') // or use env var const posts = await res.json() return { props: { posts }, revalidate: 10, } }
Why This Example Matters
This small example captures what I love about Next.js:
React works seamlessly for building the UI.
Server-side rendering comes built-in by default.
It’s easy to fetch data directly from a local database without setting up a separate backend.
Routes are automatically handled — no need for manual setup.
And all of this makes building dynamic, responsive web apps way easier
Is Next.js for You?
Personally, I think Next.js is a great fit if you’re after things like:
Fast first-page loads that feel like a traditional backend-rendered app
Smooth, SPA-like user interactions (without manually wiring up AJAX calls)
Zero-config routing that just works
A simpler deployment flow where one app handles both frontend and backend
That’s what really drew me in — it feels modern, but also practical.
Even if you already have an existing site, it’s totally possible to introduce Next.js gradually. You can start with just one page, or build out a section using micro frontends. It doesn’t have to be an all-or-nothing switch.
Of course, it’s not a silver bullet. If you’re building a pure API backend, or something super dynamic that doesn’t need SEO (like a real-time dashboard), maybe it’s not the best fit. Same goes for offline-first mobile apps. But for most modern web apps? I’d say it’s absolutely worth trying.
Want to Try It Yourself?
The official tutorial at nextjs.org/learn is excellent. I learned a lot from it and built the modal example above based on what I picked up there.
Hope this post helps you understand why I enjoy working with Next.js — and maybe inspires you to try building something with it too!
Unifa is actively recruiting, please check our website for details: