So, a while back, I decided to mess around with a small project, a Production-Order-Scheduler app, using the Next.js App Router. I wanted to keep it simple and modern, you know, using those new server actions in Next.js 14 instead of messing with API routes or extra backend stuff.
I thought Prisma would be perfect since I’ve used it before and love how it makes database stuff feel like regular TypeScript. But, man, combining Prisma with server actions was a headache at first. The docs weren’t super clear for someone like me who’s still figuring things out. After a bunch of trial and error, I got it working, and I’m gonna walk you through what I did, mistakes and all, so you can skip the frustration.
Why Bother with Prisma and Server Actions?
Here’s why I went with this setup:
Prisma is awesome for databases—it’s like writing regular code, plus it handles migrations nicely.
Server actions let you stick backend logic right in your Next.js app, so no need for separate API routes.
It’s fewer files to deal with, which means less hassle and more time to build cool stuff.
1. Starting the Project
I kicked things off by setting up a new Next.js app with TypeScript and the App Router. Here’s what I ran:
npx create-next-app@latest production-order-scheduler --ts --app
cd production-order-scheduler
Then I checked next.config.js to make sure server actions were turned on. Mine looked like this:
const nextConfig = {
experimental: {
serverActions: true,
},
};
module.exports = nextConfig;
If you don’t have that serverActions: true bit, you’ll need to add it.
2. Setting Up Prisma
Next, I got Prisma installed:
npm install prisma @prisma/client
npx prisma init
This gave me:
A prisma/schema.prisma file for my database setup.
A .env file where I put my database connection details.
A prisma folder to keep things organized.
I was using PostgreSQL on my laptop, so my .env file looked like this:
DATABASE_URL="postgresql://postgres:mypassword@localhost:5432/production-order-scheduler"
If you’re using something like MySQL or SQLite, just tweak the URL. Prisma’s got you covered.
If you’re using something like MySQL or SQLite, just tweak the URL. Prisma’s got you covered.
3. Making a Simple Schema
I wanted to keep this dead simple, so I added just one model in prisma/schema.prisma. (Here's the reference: schema.prisma on GitHub):
model Task {
id String @id @default(cuid())
title String
isDone Boolean @default(false)
createdAt DateTime @default(now())
}
Then I ran this to set up the database and create the table:
npx prisma migrate dev --name init
And just like that, my database was ready, and Prisma generated a client for me to use.
4. Setting Up the Prisma Client
Okay, here’s where I messed up at first. You can’t just keep creating new Prisma clients every time someone hits your app, or you’ll get weird errors (especially in development).
So I made a file called lib/prisma.ts to handle it properly:
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as { prisma?: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Now I can import prisma from lib/prisma.ts anywhere, and it works without breaking.
5. Writing a Server Action
Server actions are like mini backend functions you can call from your frontend. No need for separate API routes, which is super nice.
I made one to create a task in app/actions/taskActions.ts:
'use server';
import { prisma } from '@/lib/prisma';
export async function createTask(formData: FormData) {
const title = formData.get('title')?.toString();
if (!title || title.trim() === '') {
throw new Error('Gotta add a task title!');
}
await prisma.task.create({
data: { title },
});
}
The 'use server' part makes sure this only runs on the server.
6. Hooking It Up to a Form
In my app/page.tsx, I added a simple form that uses the server action:
import { createTask } from './actions/taskActions';
export default function HomePage() {
return (
<main className="p-4">
<h1 className="text-xl font-bold">Production Order Scheduler</h1>
<form action={createTask} className="my-4">
<input
type="text"
name="title"
placeholder="What’s the task?"
className="border p-2 mr-2"
/>
<button type="submit" className="bg-blue-500 text-white p-2">
Add It
</button>
</form>
</main>
);
}
Submit the form, and the task gets saved to the database. Easy peasy.
7. Showing the Tasks
To display the tasks, I made another server action in app/actions/getTasks.ts:
'use server';
import { prisma } from '@/lib/prisma';
export async function getTasks() {
return await prisma.task.findMany({
orderBy: { createdAt: 'desc' },
});
}
Then I updated app/page.tsx to show the tasks:
import { getTasks } from './actions/getTasks';
import { createTask } from './actions/taskActions';
export default async function HomePage() {
const tasks = await getTasks();
return (
<main className="p-4">
<h1 className="text-xl font-bold">Production Order Scheduler</h1>
<form action={createTask} className="my-4">
<input
type="text"
name="title"
placeholder="What’s the task?"
className="border p-2 mr-2"
/>
<button type="submit" className="bg-blue-500 text-white p-2">
Add It
</button>
</form>
<ul className="space-y-1">
{tasks.map((task) => (
<li key={task.id} className="flex justify-between">
<span>{task.title}</span>
<span>{task.isDone ? 'Done' : 'Not Done'}</span>
</li>
))}
</ul>
</main>
);
}
I kept it basic, but you could add buttons to mark tasks as done or delete them with more server actions.
What I Learned
This whole Prisma + server actions thing was way easier than I thought once I got the setup right. A few things that saved me:
Don’t create new Prisma clients all the time—use that singleton trick.
FormData is great for grabbing form inputs in server actions.
Keep your server actions in an app/actions/ folder so you don’t lose track of them.
If you’re used to splitting your backend and frontend, this setup feels kinda wild at first, but it’s honestly pretty sweet. No extra API routes, just straight-to-the-point server code.
Got questions or run into issues? Drop a comment, and I’ll try to help.