Introduction
Next.js 13 introduced the app directory with new features and conventions. This feature is still in beta, but we already have a nice overview of what the future will be like.
See https://beta.nextjs.org/docs/app-directory-roadmap for the roadmap.
Installation
To automatically create a new Next.js project using the app
directory:
npx create-next-app@latest --experimental-app
After executing this command, you will see the new project architecture :
app/
├─ globals.css
├─ head.tsx
├─ layout.tsx
├─ page.module.css
├─ page.tsx
Where is the index.tsx
file ?
To create a route for /profile
, you may be used to create a profile.tsx
file or /profile/index.tsx
. With the app directory, you now must create a new folder with a page.tsx
file.
For example, the route /profile
would be created as following :
app/
├─ globals.css
├─ head.tsx
├─ layout.tsx
├─ page.module.css
├─ page.tsx
├─ profile/
│ ├─ page.tsx
Introducing Prisma
Prisma is a next-generation Node.js and TypeScript ORM for PostgreSQL, MySQL, SQL Server, SQLite, MongoDB, and CockroachDB. It provides type-safety, automated migrations, and an intuitive data model.
Before, we may use tRPC or API routes to call Prisma. But now, with the app directory and server components, Prisma can be called directly inside the component.
Adding Prisma to the project
npm install prisma --save-dev
npx prisma init --datasource-provider sqlite
This creates a new prisma directory with your Prisma schema file and configures SQLite as your database. You're now ready to model your data and create your database with some tables.
The Prisma schema provides an intuitive way to model data. Add the following models to your schema.prisma
file:
model Todo {
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
}
We now need to migrate the database. Run the following :
npx prisma migrate dev --name init
To generate the typescript types, run :
npx prisma generate
Seed the database
Create a prisma/seed.ts
file and add the following :
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.todo.create({
data: {
title: "Learn Next.js",
},
});
await prisma.todo.create({
data: {
title: "Learn Prisma",
},
});
await prisma.todo.create({
data: {
title: "Learn GraphQL",
},
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
And in you package.json
file :
{
"name": "nextjs13-app-prisma",
"version": "0.1.0",
"private": true,
...
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
},
...
}
Now, when you run the npx prisma seed
command, it will seed your database.
Link Prisma and Next
Create a utils/prisma.ts
file :
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient()
export default prisma;
This will ensure that only one Prisma instance is running.
By default, all components are Server Components. Therefore, you can use async code in every one of them.
For example, to fetch all the to-dos in the home page, use :
import prisma from "@/utils/prisma";
export default async function Home() {
const todos = await prisma.todo.findMany();
return (
<main>
<h1 className="font-bold">Todos</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</main>
);
}
(I added Tailwind for styling, it is of course not required)
Go to localhost:3000
and you will see all your to-dos :
Data mutation
To mutate data, it is a bit more complicated. Because we need Client Component for user interactivity and because we can not have server code inside Client Component, we need to set up an API route.
Create the pages/api/todo.ts
file :
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/utils/prisma";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "PUT") {
await prisma.todo.update({
where: {
id: parseInt(req.body.id),
},
data: {
completed: req.body.completed,
},
});
res.status(200).json({ message: "Updated" });
} else {
// 404
res.status(404).json({ message: "Not found" });
}
}
In this API endpoint, we get the to-do id and state in the body and update the corresponding to-do.
We will now create a Todo Client Component.
In a app/todo.tsx
file (remember that it will not create a /todo
route) :
"use client";
import { Todo } from "@prisma/client";
import { useRouter } from "next/navigation";
export default function TodoComponent({ todo }: { todo: Todo }) {
const router = useRouter();
const update = async (todo: Todo) => {
await fetch(`/api/todo`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
completed: !todo.completed,
id: todo.id,
}),
});
router.refresh();
};
return (
<li key={todo.id} className="space-x-4">
<input
onChange={() => update(todo)}
type="checkbox"
checked={todo.completed}
/>
{todo.title}
</li>
);
}
The UI is updated using the router.refresh()
method.
Note the "use client"
at the top, it is required to let Next know that it is a Client Component.
We can then update the Home page :
import prisma from "@/utils/prisma";
import Todo from "@/app/todo";
export default async function Home() {
const todos = await prisma.todo.findMany();
return (
<main>
<h1 className="font-bold">Todos</h1>
<ul>
{todos.map((todo) => (
<Todo todo={todo}></Todo>
))}
</ul>
</main>
);
}
Enjoy your simple but working to-do in Next.js 13 using Prisma.
Find the code for the article on GitHub.