DEV Community

Mael Kerichard
Mael Kerichard

Posted on • Originally published at mael.app

Next.js 13 App directory + Prisma

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
Enter fullscreen mode Exit fullscreen mode

After executing this command, you will see the new project architecture :

app/
├─ globals.css
├─ head.tsx
├─ layout.tsx
├─ page.module.css
├─ page.tsx
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

We now need to migrate the database. Run the following :

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

To generate the typescript types, run :

npx prisma generate
Enter fullscreen mode Exit fullscreen mode

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);
  });
Enter fullscreen mode Exit fullscreen mode

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"
  },
...
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

(I added Tailwind for styling, it is of course not required)

Go to localhost:3000 and you will see all your to-dos :

Index page with 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" });
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Enjoy your simple but working to-do in Next.js 13 using Prisma.

Result

Find the code for the article on GitHub.

Top comments (0)