Why This Stack?
Prerequisites
Initialize the Next.js Project
Install Prisma and Connect PostgreSQL
Define Your Prisma Schema
Setup Prisma Client
Add Server Actions for CRUD
Build the UI
Add Loading and Error UI
Add Authentication (Optional)
Deploy to Vercel
Ongoing Improvements
Conclusion
The power of the web today lies in building applications that are performant, scalable, and developer-friendly. Next.js, combined with Prisma and PostgreSQL, offers an unparalleled full-stack development experience. And when paired with Vercel for deployment, the process becomes seamless from idea to production.
**In this in-depth guide, you’ll learn how to build a full-stack application using: **
We’ll build a simple yet scalable app: a full-featured Task Manager with authentication, CRUD operations, server actions, and a responsive UI.
Let’s dive in.
Next.js gives you SSR, SSG, API routes, App Router, and React Server Components in one powerful framework.
PostgreSQL is a battle-tested, scalable, and ACID-compliant relational database.
Prisma ORM provides type safety, easy migrations, and autocompletion with VS Code integration.
Vercel makes deploying Next.js apps incredibly easy, with edge-ready APIs, preview URLs, and environment variable management.
This stack lets you focus on product logic and features, not boilerplate setup.
We’ll use the App Router in Next.js 14 with TypeScript.
npx create-next-app@latest taskify --app --ts
cd taskify
Enable experimental features (optional, for Server Actions):
Update next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
Install Tailwind CSS:
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Update tailwind.config.js:
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
];
Add Tailwind to global styles:
/app/globals.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
Install Prisma CLI and PostgreSQL client:
npm install prisma --save-dev
npm install @prisma/client
Initialize Prisma:
npx prisma init
This creates a prisma/schema.prisma file and .env for DATABASE_URL.
Set up your database URL in .env:
DATABASE_URL="postgresql://user:password@localhost:5432/taskify"
Open prisma/schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Task {
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
}
Run the migration:
npx prisma migrate dev --name init
This generates the database and the Prisma Client.
Create /lib/prisma.ts:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: ["query"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
Now, you can use prisma in both API routes and Server Components.
/app/actions/task-actions.ts:
'use server';
import { prisma } from '@/lib/prisma';
export async function createTask(formData: FormData) {
const title = formData.get('title')?.toString();
if (!title) return;
await prisma.task.create({ data: { title } });
}
export async function deleteTask(id: number) {
await prisma.task.delete({ where: { id } });
}
/app/page.tsx:
import { prisma } from '@/lib/prisma';
import { createTask, deleteTask } from './actions/task-actions';
export default async function HomePage() {
const tasks = await prisma.task.findMany({ orderBy: { createdAt: 'desc' } });
return (
<main className="max-w-xl mx-auto mt-10">
<form action={createTask} className="flex gap-2">
<input name="title" className="border px-3 py-2 w-full" />
<button className="bg-blue-600 text-white px-4 py-2 rounded">Add</button>
</form>
<ul className="mt-6 space-y-3">
{tasks.map((task) => (
<li key={task.id} className="flex justify-between items-center bg-white p-3 rounded shadow">
<span>{task.title}</span>
<form action={() => deleteTask(task.id)}>
<button className="text-red-600">Delete</button>
</form>
</li>
))}
</ul>
</main>
);
}
/app/loading.tsx:
export default function Loading() {
return <p className="text-center mt-10 text-gray-500">Loading tasks...</p>;
}
/app/error.tsx:
'use client';
export default function Error({ error }: { error: Error }) {
return (
<p className="text-red-500 text-center mt-10">
Something went wrong: {error.message}
</p>
);
}
Install NextAuth.js:
npm install next-auth
/pages/api/auth/[...nextauth].ts:
import NextAuth from "next-auth";
import GitHubProvider from "next-auth/providers/github";
export default NextAuth({
providers: [
GitHubProvider({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
});
Use session in your app with useSession() and protect server actions.
Push code to GitHub.
Login to vercel.com and import your repository:
Bonus: Enable preview deployments for each PR.
Some ideas to evolve your app:
You have just built a production-grade, full-stack web app using Next.js 14, Prisma, and PostgreSQL and deployed it with Vercel. This tech stack gives you the scalability, performance, and developer velocity needed for modern web applications.
Next.js empowers the frontend and backend with one cohesive system. Prisma makes database modeling a joy. PostgreSQL ensures performance and data integrity. Vercel handles the ops so you can focus on code.
If you’re serious about full-stack development in 2025, mastering this stack will future-proof your skills and allow you to ship faster than ever.
Need this entire project zipped or want to add features like email auth, notifications, or Stripe integration next? Just let me know!
Software Engineer
Senior Software Engineer