Introdução
Neste tutorial, você aprenderá como construir a API REST do backend para uma aplicação de blog chamada "Median" (um simples clone do Medium). Começaremos criando um novo projeto Next.js. Em seguida iniciaremos um servidor PostgreSQL e nos conectaremos a ele usando Prisma.
Ao final, iremos construir a API REST com os seguintes endpoints:
- POST: /api/articles - Criar um novo artigo.
- GET: /api/articles - Listar todos os artigos.
- GET: /api/articles/:id - Listar um artigo específico.
- PUT: /api/articles/:id - Atualizar um artigo específico.
- DELETE: /api/articles/:id - Deletar um artigo específico.
Você encontra o código fonte do projeto neste respositório.
Tecnologias utilizadas
Iremos utilizar as seguintes ferramentas para construir esta aplicação:
- Next.js como framework de backend
- Prisma como Object-Relational Mapper (ORM)
- PostgreSQL como banco de dados
- TypeScript como linguagem de programação
Pré-requisitos
Conhecimento necessário
Este é um tutorial para iniciantes. No entanto, alguns requisitos precisam ser atendidos:
- Conhecimento básico de JavaScript ou TypeScript (preferencial)
- Conhecimento básico de Next.js
Observação: Se você não estiver familiarizado com o Next.js, poderá aprender rapidamente o básico seguindo a seção Getting Started na documentação oficial.
Ambiente de desenvolvimento
Para acompanhar este tutorial, e replicar seu conteúdo esperamos que você:
- tenha Node.js instalado.
- tenha o Prisma VSCode Extension instalado. (opcional)
- tenha acesso a um shell Unix (como o terminal/shell no Linux e macOS) para executar os comandos fornecidos nesta série. (opcional)
Primeira observação: A extensão Prisma VSCode opcional adiciona um IntelliSense muito bom e realce de sintaxe para o Prisma.
Segunda observação: Se você não tiver um shell Unix (por exemplo, se estiver em uma máquina Windows), ainda poderá acompanhar, mas os comandos do shell podem precisar ser modificados para sua máquina.
Criando o projeto Next.js
A primeira etapa do tutorial é criar o projeto Next.js, sendo assim vamos criar as pastas e inicializar o projeto com os comandos a seguir:
$ mkdir building-a-rest-api-with-nextjs-and-prisma
$ cd building-a-rest-api-with-nextjs-and-prisma
$ npx create-next-app@latest --typescript .
Responda aos questionamentos a seguir da seguinte maneira: Yes / No / Yes / Yes / ENTER.
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use Tailwind CSS with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
✔ What import alias would you like configured? … @/*
Creating a new Next.js app in /Users/dukefs/Workspace/dukescode/building-a-rest-api-with-nextjs-and-prisma.
Using npm.
Initializing project with template: app
Installing dependencies:
- react
- react-dom
- next
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next
added 273 packages, and audited 274 packages in 18s
104 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Initialized a git repository.
Success! Created building-a-rest-api-with-nextjs-and-prisma at /Users/dukefs/Workspace/dukescode/building-a-rest-api-with-nextjs-and-prisma
Após a instalação das dependências você terá um resultado semelhante a este:
├── README.md
├── next-env.d.ts
├── next.config.js
├── package-lock.json
├── package.json
├── public
│ ├── next.svg
│ └── vercel.svg
├── src
│ └── app
│ ├── api
│ │ └── hello
│ │ └── route.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.module.css
│ └── page.tsx
└── tsconfig.json
A maior parte de nosso código ficará contido dentro da pasta src/app/api
. É aqui que iremos definir os nossos endpoints e versionamentos para a API.
Podemos iniciar o noso projeto executando:
$ npm run dev
Este comando irá monitorar seus arquivos, recompilando e recarregando automaticamente o servidor sempre que você fizer uma alteração.
Para verificar se o servidor está em execução, acesse a URL http://localhost:3000/api/hello. Você deve ver uma página vazia com a mensagem "Hello, Next.js!".
Atenção: Você deve manter o servidor em execução em segundo plano conforme avança neste tutorial.
Criando uma instância do PostgreSQL
Neste momento iremos criar uma instância do PostgreSQL para utilizar como banco de dados da nossa aplicação. Você pode conferir neste tutorial Como executar o PostgreSQL no Docker com Docker Compose e persistência de dados.
Desta vez iremos utilizar um serviço em cloud que entrega uma instância do PostgreSQL por 24hrs gratuitamente, tempo necessário para realizarmos todos os nossos testes.
Para isso acesse o site Railway e na homepage cliquem em Start a New Project ou acesse diretamente https://railway.app/new.
Selecione a opção Provision PostgreSQL conforme imagem.
fig. 1 - opção para provisionamento do postgresql
Basta aguardar que o projeto será criado e a instância do PostgreSQL ficará disponível.
Com a instância do PostgreSQL criada, clique nela e selecione a aba Connect. Nesta seção teremos acesso ao DATABASE_URL que utilizaremos no futuro para conectar utilizando o Prisma.
fig. 2 - configurações da instância do postgresql
Configurando o Prisma
Agora que o banco de dados está pronto, é hora de configurar o Prisma!
Para começar, vamos primeiro instalar o Prisma CLI como uma dependência de desenvolvimento. O Prisma CLI permitirá que executemos vários comandos para interagir com o nosso projeto. Para isso execute:
$ npm install -D prisma
E para inicializar o Prisma dentro projeto executamos:
$ npx prisma init
Este comando criará um novo diretório prisma
com um arquivo schema.prisma
. Este é o arquivo de configuração principal que contém o esquema do banco de dados. Este comando também cria um arquivo .env
dentro do nosso projeto.
Atenção: O arquivo
.env
deve ser adicionado ao.gitignore
do projeto.
Vamos agora alterar nosso arquivo .env
para definir a nossa string de conexão com o banco de dados.
// .env
DATABASE_URL="postgresql://postgres:OxjXUnj5AjpCvG6wzptJ@containers-us-west-207.railway.app:7089/railway"
Atenção: A sua string de conexão será diferente da minha.
Entendendo o arquivo Schema Prisma
Ao abrir o arquivo prisma/schema.prisma
você vai ter um conteúdo semelhante a este:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
Este arquivo é escrito utilizando Prisma Schema Language
, que é um idioma que o Prisma usa para definir o esquema do seu banco de dados. O arquivo schema.prisma
tem três componentes principais:
-
Generator: Indica que você deseja gerar o Prisma Client, um type-safe query builder. Ele é usado para enviar consultas ao seu banco de dados.
-
Data source: Especifica sua conexão com o banco de dados. A configuração acima significa que seu provedor de banco de dados é PostgreSQL e a string de conexão do banco de dados está disponível na variável de ambiente
DATABASE_URL
. -
Data model: Define seus modelos de banco de dados. Cada modelo será mapeado para uma tabela no banco de daddos. No momento, não há modelos em nosso esquema, iremos explorar essa parte na próxima seção.
Observação: Para mais informações sobre o Prisma Schema você pode consultar a documentação oficial
Modelo de dados
Agora iremos definir o modelo de dados para a nossa API. Para este tutorial, iremos definir apenas um modelo. Article, esse modelo vai representar cada artigo do blog.
Dentro do arquivo prisma/prisma.schema
, adicione um novo modelo ao seu esquema chamado Article:
// prisma/schema.prisma
model Article {
id Int @id @default(autoincrement())
title String @unique
description String?
body String
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Acabamos de criar o model Article
para representar um artigo no banco de dados com vários campos. Cada campo tem um nome (id
, título
, etc.), um tipo (Int
, String
, etc.) e outros atributos opcionais (@id
, @unique
, etc.). Os campos podem se tornar opcionais adicionando ?
após o tipo de campo.
O campo id
possui um atributo especial chamado @id
. Este atributo indica que este campo é a chave primária do modelo. O atributo @default(autoincrement())
indica que esse campo deve ser incrementado automaticamente e atribuído a qualquer registro recém-criado.
O campo published
é um sinalizador para indicar se um artigo está publicado ou em modo rascunho. O atributo @default(false)
indica que esse campo deve ser definido como falso por padrão.
Os dois campos DateTime
, createdAt
e updatedAt
, rastrearão quando um artigo é criado e quando foi atualizado pela última vez. O atributo @updatedAt
atualizará automaticamente o campo com o timestamp atual sempre que um artigo for modificado.
Migration do modelo
Agora que temos o Model criado vamos enviar uma "cópia" para o banco de dados. Este processo é chamado de migration
. Durante esse processo o Prismas realiza três processos:
-
Salva a migração: O Prisma Migrate tirará uma foto da sua estrutura e descobrirá os comandos SQL necessários para realizar a migração. O Prisma salvará o arquivo de migração contendo os comandos SQL na pasta
prisma/migrations
recém-criada. -
Executa a migração: O Prisma Migrate executará o SQL no arquivo de migração para criar as tabelas no seu banco de dados.
-
Gera o Prisma Client: O Prisma gerará o Prisma Client com base na sua estrutura mais recente. Como você não possui a biblioteca Client instalada, a CLI a instalará para você também. Você deverá ver o pacote
@prisma/client
nasdependências
do seu arquivopackage.json
. Prisma Client é um construtor de query em TypeScript gerado automaticamente a partir do seu Prisma Schema. Ele é adaptado para o seu Prisma Schema e será usado para enviar consultas ao banco de dados.
Atenção: Você pode aprender mais sobre o Prisma Migrate na documentação oficial.
Vamos então executar a nossa migration com o seguinte comando:
$ npx prisma migrate dev --name "create_table_article"
Se tudo correr bem, você receberá uma mensagem de sucesso semelhante a esta:
Applying migration `20230412173740_create_table_article`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20230412173740_create_table_article/
└─ migration.sql
Your database is now in sync with your schema.
...
✔ Generated Prisma Client (4.12.0 | library) to ./node_modules/@prisma/client in 132ms
Vamos verificar o arquivo de migração gerado para ter uma visão geral do que o Prisma Migrate realiza nos bastidores.
-- CreateTable
CREATE TABLE "Article" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"body" TEXT NOT NULL,
"published" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Article_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Article_title_key" ON "Article"("title");
Atenção: O nome do arquivo de migração pode ser diferente do apresentado neste artigo.
Neste arquivo SQL, encontramos todo o necessário para criar a tabela Article
em nosso banco de dados PostgreSQL. Ele foi gerado e executado pelo Prisma baseado no seu Prisma Schema.
Voltando para a nossa instância de PostgreSQL, verificamos que a tabela realmente existe.
fig. 3 - tabela article criada em nosso banco de dados
Criando o CRUD
Finalmente vamos criar o CRUD para o modelo Article
.
Antes de efetivamente iniciarmos a criação dos endpoints, vamos criar um singleton
para o nosso Prisma Client. Sendo assim crie o arquivo client.ts
dentro de src/lib/prisma
com o seguinte conteúdo.
// src/lib/prisma/client.ts
import { PrismaClient } from '@prisma/client';
const client = new PrismaClient();
export default client;
Atenção: Os métodos aqui definidos terão validações basicas e tratativas de excepções simples. você pode melhorar isso em algum momento adicionando mais validações.
Criando um Artigo - Create
Excelente agora que temos o nosso client
criado, vamos criar o CRUD para o nosso resource Article
. Para isso crie o arquivo route.ts
dentro da pasta src/app/api/articles
.
Vamos adicionar primeiro o endpoit POST: /api/articles
para criar um novo Artigo. Abra então o arquivo route.ts
e adicione o seguinte:
// src/app/api/articles/route.ts
import client from '@/lib/prisma/client';
import { Article } from '@prisma/client';
import { NextResponse, NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const newArticle: Article = await request.json();
const createdArticle: Article = await client.article.create({
data: newArticle,
});
return new NextResponse(JSON.stringify(createdArticle), {
status: 201,
statusText: 'Created',
});
}
Perceba que neste momento apenas realizamos os imports do nosso client
para se comunicar com o banco de dados, do nosso model Article
apenas para tipar a nossa variáveis newArticle
e createdArticle
e do NextRequest
e NextResponse
para tratar as requisições e respostas HTTP's.
Criamos a função asyncrona POST
para criar um novo artigo. Nela obtemos o body do NextRequest
e passamos como parâmetro a variável newArticle
. A variável createdArticle
recebe o retorno da chamada ao nosso cliente prisma por meio client.article.create
e passa como parâmetro a variável newArticle
.
Ao final retornamos o NextResponse
com o status 201
created e o nosso Artigo recém persistido no banco de dados.
Para relizar um teste basta fazermos uma chamada HTTP POST para nossa api como a seguir.
fig. 3 - exemplo de chamada http post para a api
Perceba que a quantidade de informações recebidas é maior que a quantidade de informações enviadas. Podemos concluir que as informações adicionais vieram do nosso banco de dados.
Obtendo um ou mais Artigos - GET
Vamos agora obter uma lista de Artigos, para isso vamos continuar editando o nosso arquivo route.ts
. Abaixo, vamos criar o endpoint GET: /api/articles
para obter uma lista de Artigos.
// src/app/api/articles/route.ts
...
export async function GET() {
const articles: Article[] = await client.article.findMany();
return new NextResponse(JSON.stringify(articles), {
status: 200,
statusText: 'OK',
});
}
Desta vez apenas definimos uma função assincrona GET
para obter uma lista de Artigos. Utilizamos novamente nosso client
e por meio da função client.article.findMany
obtemos uma lista de Artigos.
Retornamos essa lista de Artigos juntamento com o status 200
ok.
fig. 4 - exemplo de chamada http get para a api listando os artigos
Para obter um artigo específico por meio do seu ID, vamos criar o arquivo route.ts
dentro da pasta src/app/api/articles/[id]
e insirir o seguinte conteúdo:
// src/app/api/articles/[id]/route.ts
import client from '@/lib/prisma/client';
import { Article } from '@prisma/client';
import { PrismaClientKnownRequestError } from '@prisma/client/runtime';
import { NextResponse, NextRequest } from 'next/server';
type FindById = {
id: string;
};
export async function GET(request: NextRequest, context: { params: FindById }) {
try {
const article: Article = await client.article.findUniqueOrThrow({
where: {
id: Number(context.params.id),
},
});
return new NextResponse(JSON.stringify(article), {
status: 200,
statusText: 'OK',
});
} catch (error) {
const msgError = (error as PrismaClientKnownRequestError).message;
return new NextResponse(JSON.stringify({ message: msgError }), {
status: 404,
statusText: 'Not Found',
});
}
}
Aqui novamente realizamos os imports necessários para tratar as requisições e respostas HTTP's. Veja também que definimos um type
, FindById
para representar o id
do artigo passado como parâmetro na url.
Finalmente utilizamos o nosso Prisma Client para obter o artigo específico por meio do seu ID utilizando o client.article.findUniqueOrThrow
. Tratamos a exception que ele pode lançar caso não encontre o Artigo solicitado.
fig. 4 - exemplo de chamada http get para a api filtrando por ID
fig. 5 - exemplo de chamada http get para a api filtrando por ID inexistente
Alterando um Artigo - PUT
Dando sequência ao nosso CRUD vamos criar agora o endpoint PUT: /api/articles/[id]
para alterar um artigo. Para isso vamos continuar editando o nosso arquivo route.ts
contido na pasta src/app/api/articles/[id]
. Adicione o seguinte conteúdo:
// src/app/api/articles/[id]/route.ts
...
export async function PUT(request: NextRequest, context: { params: FindById }) {
const newArticleData: Article = await request.json();
try {
const updatedArticle: Article = await client.article.update({
where: {
id: Number(context.params.id),
},
data: newArticleData,
});
return new NextResponse(JSON.stringify(updatedArticle), {
status: 200,
statusText: 'OK',
});
} catch (error) {
const msgError = (error as PrismaClientKnownRequestError).meta?.cause;
return new NextResponse(JSON.stringify({ message: msgError }), {
status: 404,
statusText: 'Not Found',
});
}
}
Novamente obtemos o ID
do nosso artigo pelo parametro context.params.id
e as atualizações a serem realizadas no artigo pelo body da requisição utilizando o await request.json()
.
Feito isso, vamos utilizar o client.article.update
para atualizar o artigo. Realizamos também uma tratativa de erro que pode ser lançada caso não encontre o artigo solicitado.
Ao final retornamos o artigo atualizado com o status 200
ok, ou a mensagem de erro caso não encontre o artigo solicitado com o status 404
not found.
fig. 6 - exemplo de chamada http put para a api filtrando por ID
fig. 7 - exemplo de chamada http get para a api filtrando por ID inexistente
Deletando um Artigo - DELETE
Estamos chegando na reta final do nosso artigo. Vamos agora implementar o endpoint DELETE: /api/articles/[id]
para deletar um artigo. Para isso vamos continuar deletando o arquivo route.ts
contido na pasta src/app/api/articles/[id]
. Adicione o seguinte conteúdo:
// src/app/api/articles/[id]/route.ts
...
export async function DELETE(
request: NextRequest,
context: { params: FindById }
) {
try {
await client.article.delete({
where: {
id: Number(context.params.id),
},
});
return new NextResponse(null, {
status: 204,
statusText: 'No Content',
});
} catch (error) {
const msgError = (error as PrismaClientKnownRequestError).meta?.cause;
return new NextResponse(JSON.stringify({ message: msgError }), {
status: 404,
statusText: 'Not Found',
});
}
}
Desta vez, vamos utilizar o client.article.delete
para deletar o artigo. Realizamos também uma tratativa de erro que pode ser lançada caso não encontre o artigo solicitado.
Nossos retornos possíveis são:
204
- Artigo deletado com sucesso.404
- Artigo não encontrado.
fig. 8 - exemplo de chamada http delete para a api filtrando por ID
fig. 9 - exemplo de chamada http delete para a api filtrando por ID inexistente
Chegamos ao final do nosso CRUD, lembre-se que você pode adicionar mais métodos de consulta, validações e tratamentos de erros.
Conclusão
Neste artigo criamos uma API REST com um CRUD completo para fazer a gestão de Artigos. Com métodos para listar todos os Artigos, obter um artigo específico por meio do seu id
, alterar um artigo específico por meio do seu id
e excluir um artigo específico por meio do seu id
.
Apresentamos como criar um Schema com o Prisma e como realizar o migration
para que a tabela
fosse criada no banco de dados, além é claro apresentamos como criar uma instância do PostgreSQL gratuitamente por 24hrs utilizando o Railway.
Podemos concluir que a combinação de Next.js, Prisma e PostgreSQL oferece uma stack eficiente e poderosa para criar uma API REST de alto desempenho. Com a facilidade de desenvolvimento do Next.js, a flexibilidade do Prisma e a capacidade robusta do PostgreSQL, é possível desenvolver APIs escaláveis e confiáveis para atender às necessidades de negócios e dos usuários finais.
Portanto, se você está procurando uma stack confiável para construir sua próxima API, a combinação de Next.js, Prisma e PostgreSQL é definitivamente uma opção que vale a pena considerar.
E é isso por hoje, pessoal!
Chegamos ao final de mais um artigo, espero que tenha sido útil e que você tenha aprendido algo novo.
Caso tenha alguma dúvida, comentário ou tenha encontrado algum erro, por favor, envie-me um email. Ficarei feliz em ouvir de você.
Se desejar receber novos artigos diretamente em seu e-mail, por favor, assine a nossa Newsletter. E se você já é um assinante, muito obrigado!
Aproveito e deixo um convite para nos conectarmos no Twitter e LinkedIn.
👋 Obrigado por ler até o final e até o próximo artigo !!!