Criando uma lista de contatos com Prisma

20 minutos para ler

Etapas

  1. Criação do projeto usando NextJS
  2. Criando o database com Prisma
  3. Integrando o banco com a aplicação

1 - Criação do projeto usando NextJS

Para criar o projeto usando o NextJS, vamos inicializar com o seguinte comando:

npx create-next-app@latest

Isso vai criar um projeto com a sua estrutura básica do NextJs, eu optei pelas seguintes configurações:

What is your project named? `prisma-contact-list`
Would you like to use TypeScript? `Yes`
Would you like to use ESLint? `Yes`
Would you like to use Tailwind CSS? `Yes`
Would you like to use src/ directory? `Yes`
Would you like to use App Router? (recommended) `Yes`
Would you like to customize the default import alias? `No`

Isso vai criar a estrutura necessária para o projeto, após a instalação das dependências, vamos ao VSCode e iremos apagar tudo que estiver dentro do bloco <main> dentro de src/app/page.tsx e deixaremos com essa estrutura.

<main className="flex min-h-screen flex-col items-center p-24 gap-6">
  <h2 className="text-5xl text-white font-bold">Lista de contatos</h2>

  <div className="relative overflow-x-auto shadow-md sm:rounded-lg">
    <table className="w-full text-sm text-left rtl:text-right text-gray-400">
      <thead className="text-xs text-gray-400 uppercase bg-gray-700">
        <tr>
          <th scope="col" className="px-6 py-3">
            Nome
          </th>
          <th scope="col" className="px-6 py-3">
            Telefone
          </th>
          <th scope="col" className="px-6 py-3">
            Email
          </th>
          <th scope="col" className="px-6 py-3">
            Empresa
          </th>
          <th scope="col" className="px-6 py-3">
            <span className="sr-only">Editar</span>
          </th>
        </tr>
      </thead>
      <tbody>
        <!-- aqui virá o loop de contatos -->
      </tbody>
    </table>
  </div>
</main>

Essa vai ser a estrutura padrão para listar os contatos e permitir o editar (falaremos posteriormente).

2 - Criando o database com Prisma

Agora vamos instalar o Prisma no projeto e configurá-lo para criar a base e as migrations.

Para inicializar precisamos adicionar o Prisma/Cli no projeto, basta executar

npm install prisma @prisma/client

Após a instalação e suas dependências, precisamos iniciar o Prisma no projeto, basta executar

npx prisma init

Feito isso, ele vai criar um arquivo .env na raíz do projeto parecido com esse

# Environment variables declared in this file are automatically made available to Prisma.

# See the documentation for more detail: 
https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 
# Prisma supports the native connection string format for 
PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: 
https://pris.ly/d/connection-strings 

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

Vamos usar o SQLite apenas para facilitar o processo, mas poderia ser uma base como MySQL, Postgree e etc, atualize o arquivo para isso

DATABASE_URL="file:./dev.db"

Pronto, a nossa base de dados foi definida, agora vamos a configuração dentro do schema do Prisma, vamos editar o arquivo prisma/schema.prisma na raíz do projeto.

Importante, mude o provider para sqlite - pois o padrão é postgree, senão irá receber um erro ao rodar a migration.

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

Adicione no final do arquivo a estrutura do Model que será a tabela contato no projeto.

model Contact {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  phone     String
  company   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Após rodar a migration, precisamos gerar o client para ser consumido pelo NextJS, basta rodar

npx prisma generatenpx prisma generate

Pronto, o Prisma foi criado com sucesso no projeto, agora vamos criar a API para fazer as chamadas CRUD na aplicação.

Dentro de src crie a seguinte estrutura api/contact/route.ts e vamos adicionar a estrutura para gerenciar as chamadas.

import { PrismaClient } from "@prisma/client";

export const config = {
  api: {
    bodyParser: false,
  },
};

const prisma = new PrismaClient();

export async function GET() {
  try {
    const contacts = await prisma.contact.findMany();
    return Response.json(contacts);
  } catch (error) {
    return Response.json({ error: "Erro ao obter contatos" });
  }
}

export async function POST(req: Request) {
  try {
    const { name, email, phone, company } = await req.json();

    const newContact = await prisma.contact.create({
      data: { name, email, phone, company },
    });
    return Response.json(newContact);
  } catch (error) {
    return Response.json({ error: "Erro ao criar contato" });
  }
}

export async function PUT(req: Request) {
  try {
    const { name, email, phone, company, id } = await req.json();

    const updatedContact = await prisma.contact.update({
      where: { id: Number(id) },
      data: { name, email, phone, company },
    });

    return Response.json(updatedContact);
  } catch (error) {
    return Response.json({ error: "Erro ao atualizar contato" });
  }
}

export async function DELETE(req: Request) {
  try {
    const { id } = await req.json();

    await prisma.contact.delete({
      where: { id: Number(id) },
    });

    return Response.json({ status: true });
  } catch (error) {
    return Response.json({ error: "Erro ao deletar contato" });
  }
}``` 

Com essa estrutura feita, já podemos fazer a integração com a UI, vamos dentro de src/page.txt e iremos fazer as chamadas para gerenciar a tela.

Vamos atualizar o arquivo src/page.txt para o seguinte conteúdo

"use client";
import { useState, useEffect } from "react";

type Contact = {
  id: number;
  name: string;
  email: string;
  phone: string;
  company: string;
};

export default function Home() {
  const [contacts, setContacts] = useState<Contact[]>([]);
  const [addingNew, setAddingNew] = useState(false);
  const [isEditing, setIsEditing] = useState<number | null>(null);

  const [formValues, setFormValues] = useState<Omit<Contact, "id">>({
    name: "",
    email: "",
    phone: "",
    company: "",
  });

  const handleFetchContacts = async () => {
    const res = await fetch("/api/contacts");
    const data = await res.json();

    if (data) {
      setContacts(data);
    }
  };

  const handleUpdateContact = async () => {
    const res = await fetch("/api/contacts", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ...formValues, id: isEditing }),
    });
    await handleFetchContacts();
    setIsEditing(null);
    setFormValues({ name: "", email: "", phone: "", company: "" });
  };

  const handleCreateContact = async () => {
    const res = await fetch("/api/contacts", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(formValues),
    });
    await handleFetchContacts();
    setFormValues({ name: "", email: "", phone: "", company: "" });
  };

  const handleDeleteContact = async (id: number) => {
    const res = await fetch("/api/contacts", {
      method: "DELETE",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ id }),
    });
    await handleFetchContacts();
  };

  useEffect(() => {
    handleFetchContacts();
  }, []);

  return (
    <main className="flex min-h-screen flex-col items-center p-24 gap-6">
      <div className="flex flex-col justify-center items-center gap-4">
        <h2 className="text-5xl text-white font-bold w-full flex">
          Lista de contatos
        </h2>
        <button
          onClick={() => {
            setAddingNew((prevState) => !prevState);
            setFormValues({ name: "", email: "", phone: "", company: "" });
            setIsEditing(null);
          }}
          type="button"
          className="
              py-2.5 px-2 m-auto text-sm font-medium 
              focus:outline-none rounded-lg border  
              focus:z-10 focus:ring- focus:ring-gray-700 
              bg-gray-800 text-gray-400 border-gray-600 
              hover:text-white hover:bg-gray-700 self-start
          "
        >
          Adicionar novo
        </button>
      </div>
      {(addingNew || isEditing) && (
        <form className="max-w-sm mx-auto flex gap-4 flex-col">
          <div className="flex gap-4">
            <div className="mb-5">
              <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
                Nome
              </label>
              <input
                type="text"
                id="name"
                className="
                    border text-sm rounded-lg block w-full 
                    p-2.5 bg-gray-700 border-gray-600 
                    placeholder-gray-400 text-white 
                    focus:ring-blue-500 focus:border-blue-500
                "
                required
                onChange={(e) =>
                  setFormValues({ ...formValues, name: e.target.value })
                }
                value={formValues.name}
              />
            </div>
            <div className="mb-5">
              <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
                Telefone
              </label>
              <input
                type="text"
                id="phone"
                className="
                    border text-sm rounded-lg block w-full 
                    p-2.5 bg-gray-700 border-gray-600 
                    placeholder-gray-400 text-white 
                    focus:ring-blue-500 focus:border-blue-500
                "
                required
                onChange={(e) =>
                  setFormValues({ ...formValues, phone: e.target.value })
                }
                value={formValues.phone}
              />
            </div>
          </div>
          <div className="flex gap-4">
            <div className="mb-5">
              <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
                Email
              </label>
              <input
                type="email"
                id="email"
                className="
                    border text-sm rounded-lg block w-full 
                    p-2.5 bg-gray-700 border-gray-600 
                    placeholder-gray-400 text-white 
                    focus:ring-blue-500 focus:border-blue-500
                "
                required
                onChange={(e) =>
                  setFormValues({ ...formValues, email: e.target.value })
                }
                value={formValues.email}
              />
            </div>
            <div className="mb-5">
              <label className="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
                Empresa
              </label>
              <input
                type="text"
                id="company"
                className="
                    border text-sm rounded-lg block w-full 
                    p-2.5 bg-gray-700 border-gray-600 
                    placeholder-gray-400 text-white 
                    focus:ring-blue-500 focus:border-blue-500
                "
                onChange={(e) =>
                  setFormValues({ ...formValues, company: e.target.value })
                }
                value={formValues.company}
              />
            </div>
          </div>
          {isEditing ? (
            <button
              type="button"
              className="
                  text-white bg-blue-700 hover:bg-blue-800 
                  focus:ring-4 focus:outline-none focus:ring-blue-300 
                  font-medium rounded-lg text-sm w-full sm:w-auto 
                  px-5 py-2.5 text-center
              "
              onClick={handleUpdateContact}
            >
              Editar
            </button>
          ) : (
            <button
              type="button"
              className="
                  text-white bg-blue-700 hover:bg-blue-800 
                  focus:ring-4 focus:outline-none focus:ring-blue-300 
                  font-medium rounded-lg text-sm w-full sm:w-auto 
                  px-5 py-2.5 text-center
              "
              onClick={handleCreateContact}
            >
              Criar
            </button>
          )}
        </form>
      )}

      <div className="relative overflow-x-auto shadow-md sm:rounded-lg">
        <table className="w-full text-sm text-left rtl:text-right text-gray-400">
          <thead className="text-xs text-gray-400 uppercase bg-gray-700">
            <tr>
              <th scope="col" className="px-6 py-3">
                Nome
              </th>
              <th scope="col" className="px-6 py-3">
                Telefone
              </th>
              <th scope="col" className="px-6 py-3">
                Email
              </th>
              <th scope="col" className="px-6 py-3">
                Empresa
              </th>
              <th scope="col" className="px-6 py-3">
                <span className="sr-only">Editar</span>
              </th>
            </tr>
          </thead>
          <tbody>
            {contacts?.map((contact, index) => (
              <tr
                className="
                    border-b bg-gray-800 
                    border-gray-700 
                    hover:bg-gray-600
                "
                key={index}
              >
                <th
                  scope="row"
                  className="px-6 py-4 font-medium whitespace-nowrap text-white"
                >
                  {contact.name}
                </th>
                <td className="px-6 py-4">{contact.phone}</td>
                <td className="px-6 py-4">{contact.email}</td>
                <td className="px-6 py-4">{contact.company}</td>
                <td className="px-6 py-4 text-right flex gap-4">
                  <a
                    onClick={() => {
                      setIsEditing(contact.id);
                      setFormValues({
                        name: contact.name,
                        email: contact.email,
                        phone: contact.phone,
                        company: contact.company,
                      });
                    }}
                    className="font-medium text-tertiary hover:underline"
                  >
                    Editar
                  </a>

                  <a
                    onClick={() => {
                      setIsEditing(null);
                      setFormValues({
                        name: contact.name,
                        email: contact.email,
                        phone: contact.phone,
                        company: contact.company,
                      });
                      handleDeleteContact(contact.id);
                    }}
                    className="font-medium text-tertiary hover:underline"
                  >
                    Excluir
                  </a>
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </main>
  );
}

Feito isso já verá funcionando o projeto utilizando o Prisma com NextJS.

Clique aqui para ver o projeto no GitHub.