Introdução
Neste artigo quero ensinar você a criar uma aplicação fullstack com NextJS. Registro, login, armazenamento de dados… tudo o que vem no pacote! Será, mais especificamente, uma aplicação CRUD (Create, Read, Update and Delete), isto é, um app que permite ao usuário criar, ler, atualizar e deletar informações guardadas em um servidor.
Decidi escrevê-lo porque, quando pesquisei pela primeira vez sobre o assunto, me deparei com tutoriais “para iniciantes” que atiravam no aluno inúmeros nomes e ferramentas sem justificar as escolhas ou explicar adequadamente que função elas teriam na aplicação.
“Então você instala esse pacote e escreve essa linha aqui e agora o usuário pode logar!”
Ok, está funcionando, MAS EU NÃO ENTENDI /[bad-word]/g
NENHUMA!
Compreendendo a dor de quem também está começando, tentarei ser o mais didático possível, explicando cada uma das partes móveis que eu introduzir.
Ao mesmo tempo, aviso que o propósito é criar o esqueleto de uma aplicação fullstack. Vou utilizar como base o conceito de uma lista de tarefas, mas você pode criar o que quiser enquanto lê o artigo.
Levando isso em conta, não me preocuparei com detalhes de funcionamento do React nem com estilização de componentes. Você vai aprender o que interessa: criar um banco de dados e autenticar o usuário.
O que eu preciso saber?
Conhecimento básico para intermediário de React e Next.
O básico da criação de rotas de API em Javascript.
O básico de requests de APIs com Javascript.
O primeiro requisito é essencial. Você pode tentar seguir sem os outros dois, mas sugiro que dê uma olhada no assunto antes de seguir se ainda não tem ideia de como eles funcionam.
O que utilizaremos?
NextJS
NextAuth
Prisma
MongoDB
BCrypt
Axios
SWR
Ambientação
Eu gosto de ir direto ao ponto, mas é preciso compreender minimamente as principais ferramentas que nós utilizaremos. Como afirmei acima, existem inúmeros tutoriais de criação de aplicações semelhantes à que criaremos aqui. Mas a grande maioria peca por não justificar a escolha nem explicar a natureza dos instrumentos necessários para fazê-la funcionar.
Sugiro, portanto, que você tenha paciência para se ambientar lendo os capítulos desta seção antes de pular para a parte prática.
As principais ferramentas
Nós queremos criar um sistema que permitirá ao usuário criar uma conta, salvar informações nessa conta e, depois, logar nessa conta a qualquer tempo. A nossa aplicação, portanto, tem três necessidades.
1. Um local para guardar informações
Precisamos de um banco de dados, ou database. Isso é fácil… mas nem tanto. Aqui já nos deparamos com um dilema. Queremos uma database SQL ou NoSQL? O que é isso?
SQL significa Structured Query Language, ou seja, SQL é primeiramente uma linguagem de programação. Dizemos que uma database é SQL quando todas as consultas e manipulações de dados são feitas em SQL.
A linguagem foi criada na década de 1970 e é até hoje padrão da indústria. Não por acaso. O modelo de organização utilizado em bancos SQL (modelo relacional) ainda é efetivo e relevante em praticamente qualquer cenário.
Bancos relacionais estruturam os dados em tabelas, com colunas representando propriedades e linhas representando itens individuais nos quais podemos declarar alguma relação com outros itens. Se quero organizar uma coleção de guitarras, posso criar uma nova tabela GUITARS
e adicionar colunas para classificar cada item da minha coleção:
A sintaxe também é relativamente simples: CREATE TABLE
cria uma nova tabela, SELECT TABLE
seleciona uma tabela para realizarmos as pesquisas, ou queries.
Tudo isso foi dito para situar o caro leitor, porque nós não utilizaremos SQL. Ao invés disso, criaremos a nossa database em um modelo NoSQL, que significa Not Only SQL.
E faremos assim por um ótimo motivo. É mais fácil para quem vem do front-end. O modelo relacional não só possui certas peculiaridades e conceitos (como a criação de relações entre as chaves da tabelas) que merecem artigos individuais, como ele utiliza… SQL. Outra linguagem. Com modelos NoSQL, como veremos, podemos fazer uso de algo que já conhecemos muito bem: Javascript.
E o que são modelos NoSQL? Eles surgiram no fim década de 2000 como alternativas ao modelo tradicional. Não são organizados em tabelas e linhas, mas possuem outros tipos de estrutura, como Documents e Graphs.
Nós utilizaremos o modelo de Documents, que nada mais é que o velho conhecido objeto JSON (você vai ouvir falar em BSON em alguns casos, inclusive no nosso, mas saiba que as maiores diferenças são, em favor do BSON, melhor performance e maior suporte a certos tipos de valores. A sintaxe é a mesma).
Como esse modelo funciona? Ao invés de tabelas com linhas e colunas, temos Collections com Documents. Cada Collection é como uma tabela, e cada linha como um Document. Se acima criamos uma tabela para organizar as guitarras, com o novo modelo criaremos uma Collection. Se cada linha na tabela correspondia a uma guitarra individual, no novo modelo cada guitarra corresponderá a um Document. Ao final, os mesmos dados ficariam organizados assim:
//Collection Guitars
//Você vai entender o que são esses ObjectId mais à frente.
{
"id": 1,
"name": "SG",
"brand": "Gibson",
"color": "Cherry",
},
{
"id": 2,
"name": "Telecaster",
"brand": "Fender",
"color": "Sunburst",
}
Assim é fácil visualizar, para quem já conhece a sintaxe de objetos do Javascript, os dados de cada entidade no banco de dados.
Nós criaremos a nossa database com a MongoDB, que utiliza o modelo NoSQL de Document e oferece uma opção gratuita adequada para a nossa aplicação. Cada usuário do app será um objeto como o exemplificado acima, com propriedades para e-mail, senha etc.
2. Uma ponte entre a aplicação e a database
Ótimo! Sabemos que precisamos de um banco de dados. E daí? Como eu boto as coisa lá?
Para estabelecer comunicação entre a nossa aplicação e a database, poderíamos escrever as queries com o próprio cliente da Mongo. Mas utilizaremos uma alternativa para facilitar o nosso trabalho. É aqui que entra o Prisma.
Prisma é um ORM (Object Relational Mapper), isto é, uma ferramenta que fará, por trás dos panos, o trabalho de construir, acessar e manipular os dados da database. É mais ou menos o que o React é para o Javascript em relação à manipulação do DOM. Com o Prisma, poderemos escrever códigos mais declarativos e menos imperativos. Ele não funciona apenas com a MongoDB, porque a função do Prisma é justamente abstrair as especificidades de cada database e fazê-las funcionar com a própria sintaxe.
Ele desempenhará, na nossa aplicação, duas funções:
Declaração dos esquemas dos documentos, para definir, por exemplo, que o Document de usuário será composto de um e-mail (string), uma senha (string) e uma lista de tarefas (array de strings). Pense em um schema como uma blueprint do Document.
Servirá como cliente para manipular a database, disponibilizando funções para facilmente alterar, criar e remover dados do banco.
3. Um meio de verificar se o usuário está logado e se pode acessar as informações da própria conta.
Agora, só precisamos lidar com a autenticação do usuário para proteger as nossas páginas e obter os dados do usuário logado.
Nós utilizaremos uma lib chamada NextAuth para este serviço. O NextAuth oferece meios de suprir as duas necessidades acima com funções que nos ajudam a registrar um novo usuário e a verificar o status de autenticação. Além disso, o NextAuth possui suporte para o Prisma, o que significa que ele usará o cliente e os esquemas do Prisma para essas tarefas.
Começando os trabalhos
Crie o seu projeto Next. Eu utilizei a versão 13.2.4 para escrever este artigo. Vamos instalar as dependências fundamentais do projeto.
Instalando o Prisma
Aqui vai uma dica: instale a extensão do Prisma para o VSCode se quiser dicas de sintaxe e linting no seu código.
Começamos instalando as dependências necessárias para rodar o Prisma com
npm i -D prisma
. “-D” porque nós a instalaremos apenas como uma dependência de desenvolvimento.Depois, iniciaremos o prisma com
npx prisma init
. Isto criará uma pasta chamadaprisma
com um arquivo chamadoschema.prisma
na raiz do app.Por fim, instalaremos o pacote do PrismaClient com
npm i @prisma/client
.
No schema.prisma
, modificaremos o provider para provider = "mongodb"
. O arquivo deve estar assim:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
Agora, criaremos uma pasta chamada lib
na pasta raiz do projeto. Em lib
, colocaremos alguns códigos reutilizáveis em diversos lugares na aplicação. O primeiro deles será a criação de uma instância do PrismaClient. Como usaremos o Prisma várias vezes, é útil deixar o código pronto para importá-lo conforme as nossas necessidades.
Para o Next especificamente há um adendo. Cada instância do Prisma é uma nova conexão com o servidor. Por conta do modo como funciona o hot reload do Next, precisamos definir essa instância em uma variável global para evitar que, durante o desenvolvimento, novas instâncias sejam criadas a cada compilação e gerem um erro.
O snippet abaixo serve justamente para isso: se estivermos em um abiente de desenvolvimento, utilizaremos uma instância global do Prisma. É uma solução que encontrei nesta issue levantada no repositório do Prisma no Github.
Deste modo, em um novo arquivo chamado prismadb.js
, na pasta lib
, inserimos:
import { PrismaClient } from "@prisma/client";
let prismadb;
if (process.env.NODE_ENV === "production") {
prismadb = new PrismaClient();
} else {
if (!global.prismadb) {
global.prismadb = new PrismaClient();
}
prismadb = global.prismadb;
}
export default prismadb;
A partir de agora, utilizaremos o PrismaClient com a variável prismadb
.
Criando a database com MongoDB
Vá até o site da MongoDB e crie uma conta. Uma vez logado, procure pela opção de criar database. Escolha a opção free. Crie um usuário e senha para acessar a database, selecione a opção de conexão de ambiente local e insira o seu IP.
Na próxima página, você deve procurar pelo botão “connect”. Escolha a opção de conexão pelo VSCode. O link providenciado será utilizado para que o Prisma se conecte à database.
Quando instalamos o Prisma, ele deve ter criado um arquivo .env
no diretório raiz da aplicação. Se não criou, você mesmo pode criar. O propósito deste arquivo é possibilitar declaração de variáveis disponíveis apenas no servidor, as chamadas variáveis de ambiente. Perceba que o link gerado possui o seu usuário e que você deve preencher com a sua senha. Não queremos mostrar este link ao cliente (browser), por isso nós os colocamos no .env
, que ficará assim:
DATABASE_URL="<link_disponibilizado>"
Para acessar esta variável na aplicação, basta colocarmos, no lugar do valor, process.env.DATABASE_URL. No momento da compilação ou do build do projeto, o Next se encarregará de substituir a variável pelo que foi definido no .env
.
Durante o desenvolvimento, faremos este processo para todas as variáveis de ambiente. Na produção, após o deploy, lembre-se de que você deve configurar todas as variáveis no seu serviço de hosting. Normalmente é fácil localizar esta opção.
Ah! Provavelmente o seu projeto já possui isso, mas é bom checar. No arquivo .gitignore
, verifique se .env
está declarado, para evitar que as suas variáveis sejam guardadas no Github.
Criando os models
Os models serão a estrutura das entidades que queremos guardar na database. Definiremos aqui que um usuário, por exemplo, tem e-mail, senha, lista de tarefas etc. O que faremos é definir os esquemas da database.
Esquemas, modelos, qual a diferença?
Basicamente, esquemas são a estrutura na database, e modelos a estrutura na aplicação. Porque o Prisma cria os esquemas segundo os modelos definidos na aplicação, criar um o modelo é criar o esquema.
Os modelos serão criados no arquivo schema.prisma
, criado pelo Prisma na raiz do projeto. Certifique-se de que o seu arquivo esteja assim:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
// Os modelos vão aqui.
User model
Começamos criando um model para User. Tenha em mente que o definido aqui se traduzirá em um objeto BSON como o visto acima, no exemplo das guitarras.
model User {
}
A primeira coisa que faremos é definir um ID para identificar usuários únicos. O Prisma gerará automaticamente um ID com o seguinte código, que poderá ser reutilizado em todos os modelos:
id String @id @default(auto()) @map("_id") @db.ObjectId
Note que a definição do modelo é tipada. O Prisma utiliza Typescript para garantir a precisão dos dados inseridos. Se tentássemos registrar um id como número, obteríamos um erro.
Vamos delinear o código acima.
id: é o nome da propriedade.
String: o tipo da propriedade.
@id: é um atributo do Prisma que define esta propriedade como chave primária do documento. Chaves primárias são identificadores únicos para uma entidade. No nosso caso, estamos dizendo que cada User terá o id como seu identificador único. Podemos definir mais de uma propriedade como única, mas apenas uma pode ser a chave primária.
@default(auto()): é um atributo do Prisma que gera com esta função auto() um id único como valor padrão para esta propriedade.
@map(”_id”): por este atributo o Prisma está indicando que o campo “id” deverá gerar, na MongoDB, um campo “_id”, porque “_id” é o nome padrão da chave primária na MongoDB.
@db.ObjectId: na MongoDB, o id possui um tipo “ObjectId”. Este atributo apenas diz que o campo “_id” usará o tipo específico “ObjectId”.
Ufa! O lado bom é que só escreveremos essa linha uma vez. Todos os models poderão usar o mesmo modo de geração para um id único.
Agora temos a liberdade de decidir outras propriedades para o nosso user. Quero um nome, um e-mail, uma foto de perfil, uma lista de tarefas… vamos adicioná-las:
model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String?
email String? @unique
image String?
hashedPassword String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
todoList String[]
doneList String[]
accounts Account[]
}
Definir um tipo com “?” ao final significa que aquele campo não é obrigatório. Pode parecer que qualquer usuário registrado deve ter ao menos e-mail e senha, mas mais à frente também disponibilizaremos registro por OAuth (login através do Google, Github, Facebook etc.). Nesses casos, o usuário não precisa preencher esses campos.
Você também deve ter percebido que inserimos um campo “accounts”, tipado como array de Account, no nosso modelo de User. O que fizemos foi referenciar o modelo Account, que ainda não criamos, mas que servirá para guardar os dados de quem se registrar por OAuth. É uma array porque podemos, se quiser, permitir que um único usuário logue por mais de um método.
Account model
Este modelo cuidará dos casos de autenticação OAuth. Não é preciso se preocupar com os detalhes aqui. O código abaixo é fornecido pela documentação do NextAuth para utilizar a library com o Prisma.
model Account {
id String @id @default(auto()) @map("_id") @db.ObjectId
userId String @db.ObjectId
type String
provider String
providerAccountId String
refresh_token String? @db.String
access_token String? @db.String
expires_at Int?
token_type String?
scope String?
id_token String? @db.String
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
O único ponto que merece comentário é a relação que criamos entre User e Account. Perceba como a linha user
cria esta relação. Definimos que user
é um dado do tipo User. Quem é este user? Para obter a resposta, definimos um campo (userId
) no próprio modelo Account que referenciará outro campo (id
) no modelo User. userId
deverá ter o mesmo valor que id no modelo User. Assim, o Prisma sabe que user
é um objeto do tipo User cujo id
é igual a userId
.
A opção onDelete
está marcada como Cascade
, o que significa que, se deletarmos uma Account, deletaremos o User.
Terminamos de definir os modelos! Para finalizar esta estapa, execute npx prisma db push
, e os nossos modelos já estarão na MongoDB. Sugiro entrar na database e verificar se existem duas novas Collections com os nomes User e Account.
Configurações do NextAuth
Começamos instalando o Next Auth com npm i next-auth
.
Vamos instalar também o Prisma Adapter do NextAuth com npm i @next-auth/prisma-adapter
Toda a “máquina” do NextAuth é uma API criada com o próprio Next. Os passos a seguir são exigidos pela documentação do NextAuth.
Crie, dentro da pasta “api” do Next, uma pasta chamada “auth”. Dentro de auth, crie um arquivo chamado [...nextauth].js
. Este é o arquivo de configuração do NextAuth. Nele definiremos as configurações globais e os providers (meios de registro).
A base deste arquivo será assim (não se esqueça dos imports):
import NextAuth from "next-auth"
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prismadb from "@/lib/prismadb";
export const authOptions = {
adapter: PrismaAdapter(prismadb),
providers: [],
}
export default NextAuth(authOptions)
Apenas uma nota: os meus imports que começam com ‘@’, como o do prismadb, são custom paths adicionados no jsconfig.json
. Isto é opcional. Importe os seus arquivos conforme as rotas da sua aplicação.
O primeiro provider que adicionaremos será Credentials, isto é, autenticação por credenciais (usuário e senha).
Importe Credentials:
import Credentials from "next-auth/providers/credentials"
;Adicione Credentials como um provider.
providers: [
Credentials({
id: "credentials",
name: "credentials",
credentials: {
email: {
label: "E-mail",
type: "text",
},
password: {
label: "Senha",
type: "password",
},
},
Os campos id
e name
servirão para identificar o provider quando fizermos uma request para esta API. Mais tarde, diremos “tente logar com estes dados utilizando ‘credentials’. Os campos label
e type
são utilizados pelo NextAuth para criar um formulário padrão de login. Nós vamos criar um formulário próprio, mas é bom fazer essa configuração por segurança.
Lógica de login
O que faremos agora é definir a lógica de login com credenciais. Criaremos, neste mesmo objeto de configuração de Credentials, uma função assíncrona do NextAuth chamada authorize()
para encontrar o usuário na database.
Como o encontraremos? Utilizando o PrismaClient, cuja instância criamos acima. O PrismaClient possui um método chamado findUnique
. Com ele, dizemos “onde” procurar (where) e o que procurar, e então ele retorna o objeto encontrado:
const user = await prismadb.user.findUnique({
where: {
email: credentials.email,
},
});
A função auhtorize()
é basicamente um handler de API, ao qual enviaremos os dados de login (email e senha) e do qual obteremos uma resposta, que será o usuário.
Você notará que estamos utilizando uma função chamada compare()
para verificar a senha do usuário ao declarar a variável isCorrectPassword
. Quando criarmos a lógica de registro, usaremos um pacote chamado bcrypt
para criptografar as senhas. A função compare() é uma função do próprio bcrypt
para fazer a validação com base na senha criptografada.
Assim, instale bcrypt com npm i bcrypt
e importe compare: import { compare } from "bcrypt"
.
A função auhtorize()
deverá ser adicionada como outro objeto de Credentials:
// ...
// type: "password",
// },
// },
async authorize(credentials) {
try {
//Lidamos com o caso de ausência das credenciais
if (!credentials.email || !credentials.password) {
throw new Error("É preciso preencher o usuário e a senha.");
}
//Busca um usuário na db
const user = await prismadb.user.findUnique({
where: {
email: credentials.email,
},
});
//Se não encontra, retorna um erro.
if (!user) {
throw new Error("O usuário não existe.");
}
//Se o usuário existe, mas não possui senha, significa que ele se registrou com outro método.
if (!user.hashedPassword) {
throw new Error(
"Parece que você se registrou com um método diferente."
);
}
// Verifica se o password enviado no login bate com o password criptografado na database.
const isCorrectPassword = await compare(
credentials.password,
user.hashedPassword
);
if (!isCorrectPassword) {
throw new Error("Senha incorreta.");
}
return user;
} catch (error) {
throw new Error(error.message);
}
},
Você pode estar se perguntando: “ok, mas como eu enviarei essas informações? De onde elas sairão?”. O próprio NextAuth oferece uma função para enviar os dados de login chamada signIn
. Nós a criaremos mais tarde, e passaremos credentials
e os dados de login como argumentos para que a lógica criada acima seja utilizada.
Depois do snippet acima, falta definirmos a estratégia para manter a sessão do usuário. Nós utilizaremos JWT, que significa JSON Web Token. Quando o usuário logar, a aplicação criará este token como um cookie no browser. O NextAuth lerá esse cookie a cada acesso e decidirá se a sessão é válida ou não.
Como tudo o que deve permanecer em segredo, definiremos a chave de geração do JWT nas variáveis de ambiente. Você decide o que será esse segredo. Uma boa sugestão é bater a cabeça no teclado e gerar uma chave segura, ou utilizar outros geradores confiáveis.
// ...
// throw new Error(error.message);
// }
// },
session: {
strategy: "jwt",
},
jwt: {
secret: process.env.NEXTAUTH_JWT_SECRET,
},
secret: process.env.NEXTAUTH_SECRET,
E então iremos ao arquivo .env
e declararemos as chaves criadas.
NEXTAUTH_JWT_SECRET="c56C+XzTJicxQZBdP3bViDf+ARmHJpFYy7prOg3Ji/wdx9uIxdpMLaL+JYgOkU"
NEXTAUTH_SECRET="h393jmE)_-o3mopdmnh#*4jh0m3JSlasl,*5nksp08nmjls@,lsjdej3pa9#çsj932"
Por fim, vamos definir uma página padrão para onde o usuário será redirecionado caso tente acessar uma rota protegida, ou fazer o login e o servidor não validar a sessão. Você pode definir a página que quiser aqui (/signin, por exemplo):
//...
// secret: process.env.NEXTAUTH_SECRET,
pages: {
signIn: "/signin",
},
Ah! Se quiser depurar o seu código de forma que os logs apareçam no prompt local, pode adicionar esta propriedade também, depois de pages:
debug: process.env.NODE_ENV === "development",
O nosso [...nextauth].js
ficará assim (por enquanto):
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { compare } from "bcrypt";
import prismadb from "@/lib/prismadb";
export const authOptions = {
adapter: PrismaAdapter(prismadb),
providers: [
Credentials({
id: "credentials",
name: "credentials",
credentials: {
email: {
label: "E-mail",
type: "text",
},
password: {
label: "Senha",
type: "password",
},
},
async authorize(credentials) {
try {
if (!credentials.email || !credentials.password) {
throw new Error("É preciso preencher o usuário e a senha.");
}
const user = await prismadb.user.findUnique({
where: {
email: credentials.email,
},
});
if (!user) {
throw new Error("O usuário não existe.");
}
if (!user.hashedPassword) {
throw new Error(
"Parece que você se registrou com um método diferente."
);
}
const isCorrectPassword = await compare(
credentials.password,
user.hashedPassword
);
if (!isCorrectPassword) {
throw new Error("Senha incorreta.");
}
return user;
} catch (error) {
throw new Error(error.message);
}
},
}),
],
pages: {
signIn: "/signin",
},
debug: process.env.NODE_ENV === "development",
session: {
strategy: "jwt",
},
jwt: {
secret: process.env.NEXTAUTH_JWT_SECRET,
},
secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);
Lógica de registro
Para registrar um usuário, vamos criar uma API na nossa aplicação. Se você não sabe criar APIs com Next, saiba que é como criar APIs com Node, com o adendo de que as rotas e controles são definidos no mesmo arquivo dentro da pasta API. Se você não sabe criar APIs com Node, sugiro que dê uma olhada no assunto antes de continuar.
Na pasta API, então, criaremos uma rota para o registro com um arquivo chamado register.js
. Utilizaremos o cliente do Prisma para verificar a existência do usuário e registrá-lo, caso ele não exista, com o método create
, que recebe um objeto data{}
como argumento (os nossos dados):
const user = await prismadb.user.create({
data: {
name,
email,
hashedPassword,
},
});
Faremos uso, também, do bcrypt
mencionado acima, para enviar a senha já criptografada à database.
Esta criptografia será feita com a função hash
do pacote, que recebe uma string e um número como argumentos. A string é o que será criptografado, e o número a quantidade de vezes que a função será executada sobre essa string para criptografá-la. Valores maiores podem significar maior segurança, mas também significam mais recursos computacionais utilizados. O valor 12 nos dá um bom equilíbrio entre segurança e performance.
Nesta parte, devemos decidir o que será registrado. Se você fez como eu e adicionou apenas e-mail, senha e nome como dados relevantes para o modelo User, então queremos registrar aqui apenas estes campos.
No arquivo register.js
, portanto, criaremos a seguinte API:
import bcrypt from "bcrypt";
import prismadb from "@lib/prismadb";
export default async function handler(req, res) {
// Se o método não for POST, retorna 405 (método não permitido).
if (req.method !== "POST") {
return res.status(405).end();
}
try {
// Desestrutura os dados enviados no body da requisição
const { name, email, password } = req.body;
// Se, por algum acaso, a validação do formulário não funcionar, a API garante que pelo menos o Nome, a Senha e o Email estão preenchidos para o registro.
if (!name || !password || !email) {
return res.status(401).json({ error: "É preciso preencher todos os campos." });
}
// findUnique seleciona, dentro da collection User, o usuário cujo e-mail seja igual ao e-mail passado na requisição. Definir apenas "email" é uma abreviação de "email: email".
const existingUser = await prismadb.user.findUnique({
where: {
email,
},
});
if (existingUser) {
return res.status(422).json({ error: "Este e-mail já foi cadastrado." });
}
// Criptografa a senha
const hashedPassword = await bcrypt.hash(password, 12);
// Cria o usuário na database
const user = await prismadb.user.create({
data: {
name,
email,
hashedPassword,
},
});
return res.status(200).json(user);
} catch (error) {
return res.status(400).json({ error: "Algo deu errado :(" });
}
}
Criando o formulário
Já configuramos o login e o registro, mas ainda não proporcionamos um meio para que o usuário realize essas ações.
Como o propósito deste artigo é ensiná-lo a manejar o login e o registro de dados em databases, não me preocuparei em explicar os detalhes de criação de formulários com React, como funciona o controle de states etc., mas apontarei o que for relevante para a sua criação. Eu também providenciei o código completo de um formulário abaixo para você ter uma base.
A primeira coisa de que precisaremos é um state para definir se o formulário é de registro ou login. Você pode fazer um formulário para cada situação. Eu prefiro criar um só e mudar este state para definir qual das funções será chamada:
const [formType, setFormType] = useState("login");
O essencial mesmo é um state para armazenar os valores dos inputs (se você está seguindo o artigo passo a passo, bastam esses campos):
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
E uma função para onChange de cada input, de forma que os campos preenchidos tenham o mesmo valor de formData.
function handleChange(e) {
setFormData((prev) => {
return { ...prev, [e.target.id]: e.target.value };
});
}
Por fim, um botão para enviar os dados às funções de registro e login (que criaremos a seguir):
function handleSubmit(e) {
// Não se esqueça de definir este preventDefault
// para evitar o reload da página quando o usuário clicar no botão
e.preventDefault();
if (formType === "register") {
register();
} else if (formType === "login") {
login();
}
}
Se quiser, pode criar desde já um botão que registrará o usuário com o Google e outro com o Github. Nós lidaremos com essas opções mais à frente.
Eis um formulário que criei, se você quiser uma representação visual:
O código completo ficaria mais ou menos assim (sem estilos):
export default function Form() {
const [warning, setWarning] = useState("warning");
// State que define o tipo do formulário
const [formType, setFormType] = useState("login");
// State que guarda os valores dos inputs
const [formData, setFormData] = useState({
name: "",
email: "",
password: "",
});
function handleChangeFormType(type) {
setWarning("warning");
setFormType(type);
}
// Função que muda o state para guardar os inputs
function handleChange(e) {
setFormData((prev) => {
return { ...prev, [e.target.id]: e.target.value };
});
}
// Função que decide a função a ser chamada
function handleSubmit(e) {
e.preventDefault();
setIsLoading(true);
if (formType === "register") {
register();
} else if (formType === "login") {
login();
}
}
// JSX do formulário
return (
<>
<div>
<div>
<button onClick={() => handleChangeFormType("login")}>
Login
</button>
<button onClick={() => handleChangeFormType("register")}>
Cadastro
</button>
</div>
<form onSubmit={handleSubmit}>
{formType === "register" && (
<input
type="text"
placeholder="Nome"
id="name"
onChange={handleChange}
required
/>
)}
<input
type="email"
placeholder="E-mail"
id="email"
onChange={handleChange}
required
/>
<input
type="password"
placeholder="Senha"
id="password"
onChange={handleChange}
required
/>
<p>
{warning}
</p>
<button type="submit">
{formType === "login" ? "Login" : "Cadastro"}
</button>
</form>
<div>
<button onClick={() => handleOAuth("google")}>
<FcGoogle />
</button>
<button onClick={() => handleOAuth("github")}>
<IoLogoGithub />
</button>
</div>
<p>
* Esta é uma aplicação de estudos. A senha é criptografada. Todos os dados
podem ser deletados sem aviso prévio.
</p>
</div>
</>
);
}
Função de registro
Vamos criar a função de registro por credenciais neste mesmo componente do formulário. Esta função será assíncrona, mais especificamente uma request do tipo POST feita à API register.js
que criamos anteriormente.
Nós a declararemos dentro de um useCallBack porque ela receberá variáveis do formulário, e só queremos que a função seja recriada se o usuário escrever algo nos inputs.
A base dela será assim:
const register = useCallBack(async()=> {
}, [formData])
Para fazer a request eu utilizarei Axios, que é uma library criada para facilitar manejo de APIs em Javascript. Instalá-lo não é necessário e você pode usar a função fetch()
nativa sem problemas. Se quiser seguir os mesmos passos que eu, instale npm i axios
e o importe no componente com import axios from "axios"
.
O que faremos, então, é uma simples request do tipo POST para a nossa API de registro. Lá nós dissemos que o body da request conteria name, password e email. Portanto, enviaremos estes campos no body da nossa request. Como o meu state formData
contém esses campos já em um objeto ( {name: "nome", email: "email", password: "senha"}
), posso enviá-lo diretamente.
const register = useCallback(async () => {
try {
await axios.post("/api/register", formData);
login();
} catch (error) {
console.log(error);
setWarning(error.response.data.error);
}
}, [formData, login]);
Note que dentro do bloco catch
estou alterando um state do componente para alertar ao usuário a ocorrência de algum erro. O objeto error.response.data.error
, é como acesso o texto do erro retornado pelo Axios, e o texto deste erro definimos lá na API. Assim, se o usuário já existe, por exemplo, o formulário mostrará a mensagem "Este usuário já existe.”.
Você também deve ter notado que chamei a função login()
logo após a request. Fiz isto para que o usuário seja automaticamente autenticado caso o registro ocorra. Ainda não criamos esta função, mas já é possível testar o nosso registro. Remova a call de login() por enquanto e tente se registrar pela sua aplicação, verificando se um novo Document de usuário aparece na MongoDB. Se tudo estiver funcionando, vamos ao próximo passo.
Função de login
Para criar a função de login, utilizaremos uma função fornecida pelo NextAuth chamada signIn()
.
Começamos importando-a: import {signIn} from "next-auth/react"
O método signIn()
recebe dois argumentos: o id do provider e um objeto com os dados de login. Lembre-se que nós definimos no arquivo de configuração do NextAuth “credentials” como o id do provider de credenciais. Quanto ao segundo argumento, passaremos email e password, retirados do nosso state que guarda os valores dos inputs.
Também passaremos a opção redirect: false
para evitar que o usuário seja redirecionado na tentativa de login. Vamos nós mesmos lidar com esse redirecionamento usando o roteamento do Next.
const login = useCallback(async () => {
try {
const res = await signIn("credentials", {
email: formData.email.trim(),
password: formData.password.trim(),
redirect: false,
});
if (res.ok) {
router.push("/user");
} else {
throw new Error(res.error);
}
} catch (error) {
setWarning(error.message);
}
}, [formData, routerl]);
Deixe-me explicar o snippet acima. A função signIn()
retorna um objeto. Se este objeto contiver a propriedade ok
, significando que a request foi bem sucedida, vamos redirecioná-lo para a página de usuário na nossa aplicação, que eu criei no path “/user”. Se a resposta não for ok
, jogará um erro que será lido pelo bloco catch
e mudará o state warning
que eu criei para mostrar eventuais erros de login.
Pronto! Agora o usuário já pode logar! Crie a página para a qual o usuário será redirecionado na sua aplicação e teste!
Quero fazer apenas um adendo aqui. Certifique-se de ter declarado a função login
antes de register
. Como estamos chamando a função de login dentro da função de registro, ela precisa ser inicializada antes.
//primeiro login
const login = useCallback...
//depois register
const register = useCallback...
Adicionando outros tipos de autenticação
- Esta parte é opcional. Se você não pretende adicionar login com métodos diferentes, pode pular para a próxima seção.
Nós fizemos a parte mais difícil, que é criar autenticação com credenciais. Vamos adicionar alternativas OAuth para o usuário, como Google e Github. OAuth significa Open Authorization. É uma forma de reutilizar os dados já registrados em uma aplicação terceirizada para registrar o usuário na sua própria aplicação. Nestes casos, a senha utilizada na aplicação terceirizada não é exposta.
Começamos incluindo lá no [...nextauth].js
os providers do Github e do Google.
Importamos os providers:
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
E, dentro da array de providers, onde definimos Credentials, definiremos os novos providers:
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID || "",
clientSecret: process.env.GITHUB_SECRET || "",
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
// Credentials({
// ...
Só isso não basta. Precisamos agora pedir autorização desses providers para utilizar os seus serviços. Com a autorização, obteremos as variáveis de ambiente indicadas acima para clientId
e clientSecret
.
Github
Primeiro, devemos criar uma chave de autenticação no Github. No site do Github, encontramos esta opção em Settings > Developer Settings > OAuth Apps. Preenchemos o nome da nossa aplicação (que aparecerá ao usuário). Os campos de Homepage URL e Authorization callback URL podemos preencher com “http://localhost:3000/” (ou a URL local que você está utilizando) já que estamos em um ambiente de desenvolvimento.
Guardamos, então, as chaves de ID e o Secret do cliente nas nossas variáveis de ambiente com os nomes de variável definidos nas configurações do NextAuth (GITHUB_ID e GITHUB_SECRET, no caso do nosso exemplo).
A única diferença será o modo de criação das chaves. Para o Google, entramos na plataforma da Google Cloud e criamos um Novo Projeto. Certificamo-nos de que este projeto está selecionado, e procuramos pela opção APIs e serviços (pode-se utilizar o campo de pesquisa do site). No menu lateral, selecionamos Tela de permissão OAuth. Escolhemos uso Externo e depois preenchemos o formulário com as informações da nossa aplicação.
Como estamos em um ambiente de desenvolvimento, na parte de Escopos e Usuários de teste não precisamos preencher nada.
Agora, no menu lateral selecionamos Credenciais e clicamos em Criar Credenciais > ID do Cliente OAuth. Selecionamos Aplicação Web e definimos o nome da aplicação.
Agora, o único campo que precisamos preencher será Adicionar URI. Nele, devemos adicionar este link, utilizado pelo NextAuth:
http://localhost:3000/api/auth/callback/google
Lembrando sempre que utilizamos “localhost:3000” por estarmos em um ambiente local de desenvolvimento. Se você fizer deploy do seu site em algum momento, será preciso alterar todas as configurações dos providers e das variáveis de ambiente para a URL de produção.
Lidando com o login por OAuth
Vamos criar a última função do nosso formulário para lidar com o caso autenticação por OAuth.
Se você ainda não criou um botão para o Google, Github ou outros providers no seu formulário, a hora é agora.
Será uma função simples função assíncrona que chamará signIn
com o id do provider como argumento. Também passaremos um objeto como segundo argumento, com a propriedade callBackUrl
definindo a URL de redirecionamento caso a autorização seja um sucesso.
Um bom modo de criar esta função é passar provider
como parâmetro e alterar o argumento na call da função conforme o provider, deste modo:
async function handleOAuth(provider) {
try {
await signIn(provider, { callbackUrl: "/user" });
} catch (error) {
console.log(error);
}
}
Segundo a documentação do NextAuth, o id do provider do Google é google
, e do Github é github
. Para outros casos, cheque a documentação.
Finalmente, para chamar esta função definimos um onClick nos nossos botões, por exemplo: onClick={() => handleOAuth("google")}
.
Pronto, agora o usuário poderá logar com Credenciais, Github e Google na nossa aplicação!
Protegendo as nossas rotas
Precisamos nos certificar de que apenas usuários autenticados consigam acessar certas rotas. Para isso, vamos utilizar o getServerSideProps
do Next e uma função chamada getServerSession
do NextAuth.
Como sabemos, o Next fornece a possibilidade de se executar funções no servidor antes da renderização da página com getServerSideProps
. Não queremos passar props para o componente, mas utilizaremos a função para verificar se o cliente possui uma sessão ativa antes de mostrar qualquer conteúdo. Se não houver, o usuário será redirecionado para a nossa página de registro ou login.
Como verificar se há uma sessão ativa? Obteremos a resposta através de getServerSession. Esta função retorna um objeto com algumas informações do usuário da sessão atual, como nome e e-mail. Precisamos passar a ela req
e res
como argumentos, que nós acessaremos a partir do context
do getServerSideProps
. O terceiro argumento são as opções do NextAuth, que foram exportadas no arquivo […nextauth].js
.
Nas páginas que queremos proteger (aquelas em que o usuário deve estar logado para visualizar), vamos fazer a verificação no lado do servidor. A função a seguir deve ser declarada após os imports e antes do componente da página (como todo getServerSideProps
):
import { getServerSession } from "next-auth";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
export async function getServerSideProps(context) {
const session = await getServerSession(context.req, context.res, authOptions);
if (!session) {
return {
redirect: {
destination: "/signin",
},
};
}
return {
props: {},
};
}
Deste modo, se não houver session
válida, o usuário será redirecionado para a página escolhida.
Obtendo os dados do usuário
Já temos uma lógica para registro e login, e já estabelecemos um meio de proteger as nossas rotas. O que precisamos, agora, é obter os dados do usuário atual para renderizá-los no cliente.
Esta parte pode ficar um pouco confusa, então preste atenção: para obter esses dados, vamos criar mais uma API. E que lógica ela precisa conter? Algo que nos permita selecionar o usuário atual na database. Poderíamos criar esta lógica diretamente na API, mas pense lá na frente: toda vez que quisermos alterar alguma informação sobre o usuário precisaremos selecioná-lo antes. Obter os dados? Selecione o usuário e retorne. Adicionar uma tarefa? Selecione o usuário e adicione.
Vamos, por isso, criar uma outra lib, que retornará o nosso usuário atual para utilizá-lo em todas as nossas APIs.
Depois, vamos criar uma API apenas para obter o usuário atual. Ela será nossa rota current
.
Após estes passos, criaremos finalmente um meio para utilizar os dados obtidos no cliente.
Função para ser usada nas APIs
Na pasta lib
vamos criar um arquivo chamado serverAuth.js
e escrever uma função que retornará o usuário selecionado.
Nesta função, utilizaremos mais uma vez getServerSession
do NextAuth para obter o e-mail do usuário. Os parâmetros req
e res
serão supridos porque chamaremos serverAuth apenas dentro das nossas APIs, que já possuem essas informações como parâmetros. A call da API poderá enviar req
e res
como argumentos para que o NextAuth cuide de obter os dados da sessão.
Obtida a sessão, temos o e-mail do usuário logado. Com este e-mail, utilizaremos findUnique
do PrismaClient para selecionar o usuário da database. Se esse usuário for encontrado, vamos retorná-lo. A função ficará assim:
import prismadb from "./prismadb";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/pages/api/auth/[...nextauth]";
const serverAuth = async (req, res) => {
//obtém dados da sessão
const session = await getServerSession(req, res, authOptions);
//se não há sessão, retorna um erro
if (!session.user.email) {
throw new Error("O usuário não está logado.");
}
// seleciona o usuário com o cliente do Prisma
const currentUser = await prismadb.user.findUnique({
where: {
email: session.user.email,
},
});
//se assegura de que um usuário foi selecionado
if (!currentUser) {
throw new Error("O usuário não existe.");
}
//finalmente, retorna o nosso usuário
return { currentUser };
};
export default serverAuth;
Criando a API
Na pasta api
do Next, vamos criar uma nova rota com o nome current.js
. Ela apenas retornará o currentUser
de serverAuth, conforme as explicações acima.
export default async function handler(req, res) {
if (req.method != "GET") {
return res.status(405).end(); //incorrect request type
}
try {
const { currentUser } = await serverAuth(req);
return res.status(200).json(currentUser);
} catch (error) {
console.log(error);
return res.status(400).end();
}
}
Utilizando useSWR e um custom hook
A lógica da API está criada. É hora de criar a lógica para obter os dados no cliente. Para isto, utilizaremos SWR (Stale-While-Revalidate), uma lib de data fetching criada pela Vercel (a mesma companhia por trás do Next) que nos permitirá utilizar os dados obtidos com facilidade.
Funciona da seguinte maneira: o hook useSWR
faz uma request para uma API e armazena o resultado no cache do browser. A URL passada como argumento para o hook (a rota da nossa API, no caso) se torna uma ‘key’ daquela request compartilhada por toda a aplicação. Se este hook for utilizado com a mesma chave em outro local do app, os dados serão obtidos diretamente do cache e não a partir de outra request. É uma forma de compartilhar o usuário por toda a aplicação sem precisar usar contextos.
O hook possui muitas opções e é bastante otimizado. É possível definir, por exemplo, que a revalidação será feita em um determinado intervalo de tempo ou a cada mudança de tela no browser. Ele também fornece uma variável que indica o estado da request (isLoading
) e uma função para modificar os dados manualmente, sem a necessidade de outra request (a função mutate
), o que é muito útil para renderizar automaticamente dados modificados na database.
E por que estou falando em “custom hook”? Vamos criar um hook para utilizar useSWR. Pode parecer exagero, mas fazer isto economizará várias linhas quando quisermos acessar os dados do usuário na aplicação.
Criando o hook
Crie uma nova pasta chamada hooks. Dentro dela, crie um arquivo chamado useCurrentUser.js
.
Um custom hook do React nada mais é que um componente que pode agrupar o resultado de outros hooks e funções em um único arquivo para ser usado em vários lugares da aplicação.
Vamos instalar o pacote do SWR com npm i swr
, e importá-lo com import useSWR from "swr"
.
useSWR
recebe como argumentos a URL da nossa API e uma função para fazer a request. A URL é a rota /api/current
que criamos. A função é um fetch GET simples para a API, que nós definiremos com Axios. Importe, portanto, o Axios no seu arquivo.
O hook ficará assim:
import useSWR from "swr";
import axios from "axios";
const useCurrentUser = () => {
const fetcher = (url) => axios.get(url).then((res) => res.data);
const { data, error, isLoading, mutate } = useSWR("/api/current", fetcher);
return {
data,
error,
isLoading,
mutate,
};
};
export default useCurrentUser;
Perceba que useSWR retorna data
, error
, isLoading
e mutate
, e nós estamos retornando esses valores no nosso próprio hook.
data: é o objeto do usuário na database. Este valor contém tudo o que o modelo User contém.
error: objeto que contém eventuais erros retornados da request.
isLoading: state que é
true
durante a request efalse
quando ela é finalizada.mutate: função que permite a revalidação dos dados no cache e a sua manipulação.
Testando a request
É a hora da verdade. Suponho que você já tenha se registrado e esteja logado na aplicação. Vá até a página que você protegeu com getServerSideProps
e importe useCurrentUser
.
Chame o hook desta forma:
const { data: user, error, isLoading, mutate } = useCurrentUser()
Ele retorna o objeto data
, que é o nome padrão do useSWR
. O que fizemos acima foi apenas renomear data
para user
, mas isto não é obrigatório.
Dê um log para verificar o que é o objeto user e renderize, em um HTML qualquer, o nome do usuário:
<p> Olá, {user?.name}!</p>
Note que adicionamos uma condicional ?
a user
. A sintaxe acima é o equivalente a user && user.name
, isto é, “se user for truthy, renderize user.name”. Isto garante que o Next não vai tentar renderizar uma variável indefinida.
Se você quer adicionar outra camada de segurança ou sinalizar visualmente ao usuário que os dados ainda não retornaram, pode renderizar seus componentes de acordo com o valor de isLoading
e error
:
export default function UserPage() {
const { data: user, error, isLoading, mutate } = useCurrentUser()
if (error) {
return <p>Oops, algo deu errado!</p>
}
return (
<>
{isLoading ? <p>Carregando...</p> : <p> Olá, {user?.name}!</p>}
<button onClick={() => signOut()}>Sair</button>
</>
);
}
Adicionei no snippet acima um botão para deslogar. Ela utiliza a função signOut
do NextAuth. Basta chamá-la para que a library cuide de remover a sessão atual.
Adicionando uma tarefa
Os fundamentos da aplicação estão construídos! O usuário já pode se registrar e logar, já protegemos nossas páginas de usuários não autenticados e já estamos renderizando os dados da database no cliente.
Falta apenas adicionar um método de manipulação dos dados. Criaremos, então, uma API para que o usuário possa adicionar uma tarefa na database (lembrando que estou utilizando uma lista de tarefas apenas como plano de fundo deste artigo).
Criando a API
Já temos tudo o que precisamos para incluir essa finalidade. Utilizaremos o cliente do Prisma para adicionar a tarefa.
Na pasta api
, criaremos uma rota chamada todo.js
(de to do, isto é, uma tarefa “para fazer”) e escreveremos o esqueleto do handler. Como estamos adicionando uma tarefa, vamos criar nossa lógica para o método POST:
import prismadb from "@/lib/prismadb";
import serverAuth from "@/lib/serverAuth";
export default async function handler(req, res) {
try {
if (req.method === 'POST') {
}
} catch (error) {
console.log(error);
return res.status(400).json("Oops! Algo deu errado.");
}
}
Dentro da condicional vamos obter o usuário atual com o serverAuth
que criamos para utilizar em todas as APIs (lembre-se que serverAuth retorna o currentUser):
const { currentUser } = await serverAuth(*req*, *res*);
Quando criarmos um formulário para adicionar a tarefa, passaremos uma tarefa (string) no body da request. Vamos obter essa tarefa desestruturando o body:
const { todo } = req.body;
Vamos adicionar a tarefa na lista do usuário, utilizando o método update
do Prisma, que nos permite adicionar diretamente um elemento em uma array com push
:
const response = await prismadb.user.update({
where: {
email: currentUser.email,
},
data: {
todoList: {
push: todo,
},
},
});
return res.status(200).json("Adicionado com sucesso.");
Ficará assim:
import prismadb from "@/lib/prismadb";
import serverAuth from "@/lib/serverAuth";
export default async function handler(req, res) {
try {
const { todo } = req.body;
if (req.method === 'POST') {
const { currentUser } = await serverAuth(req, res);
const response = await prismadb.user.update({
where: {
email: currentUser.email,
},
data: {
todoList: {
push: todo,
},
},
});
return res.status(200).json("Adicionado com sucesso.");
}
} catch (error) {
console.log(error);
return res.status(400).json("Oops! Algo deu errado.");
}
}
Lógica no cliente
Agora, vá até a sua página e crie uma função assíncrona para fazer uma request do tipo POST à rota (estou utilizando Axios):
async function addTodo(todo) {
try {
const res = await axios.post("/api/todo", {
todo: todo,
});
} catch (error) {
console.log(error.message);
}
}
Depois, crie um input e um botão simples para que o usuário digite a tarefa e chame a função com o valor do input. Aproveite e faça uma ul
para mostrar as tarefas adicionadas na database, utlizando o campo todoList
de user
, que já obtivemos através do hook useCurrentUser
. Com base na página que criamos anteriormente, as adições a deixarão mais ou menos assim:
export default function UserPage() {
const { data: user, error, isLoading, mutate } = useCurrentUser()
const [todo, setTodo] = useState('');
async function addTodo(todo) {
try {
const res = await axios.post("/api/todo", {
todo: todo,
});
console.log(res.data);
} catch (error) {
console.log(error.message);
}
}
if (error) {
return <p>Oops, algo deu errado!</p>
}
return (
<>
{isLoading ? <p>Carregando...</p> : <p> Olá, {user?.name}!</p>}
<button onClick={() => signOut()}>Sair</button>
<input type="text" value={todo} onChange={(e) => setTodo(e.target.value)} />
<button onClick={() => addTodo(todo)}>Adicionar tarefa</button>
<ul>
{user.todoList.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
</>
);
}
Eu adicionei a tarefa, mas ela não aparece na minha página!
A tarefa foi adicionada à database. Se você recarregar a página, verá que ela será renderizada. O problema é que estamos apenas atualizando a database sem avisar a aplicação.
Nós poderíamos criar um state apenas para armazenar a lista de tarefas, e incluir nesse state cada nova tarefa adicionada, mas o SWR nos fornece uma boa alternativa.
Como mencionado anteriormente, chamar mutate
força o SWR a revalidar os dados no cache e re-renderizar o componente. Poderíamos, então, simplesmente chamar mutate()
na função addTodo
, logo após a request (ainda dentro de try
, é claro).
No entanto, fazer isto pode comprometer a performance, pois uma nova request será feita cada vez que um dado for alterado. Graças à engenhosidade da Vercel, mutate
pode receber os dados novos como argumento incluí-los no cache sem realizar outra request. Vamos atualizar a função addTodo
:
async function addTodo(todo) {
try {
const newTodos = {...user, todoList: [...user.todoList, todo]}
const res = await axios.post("/api/todo", {
todo: todo,
});
mutate(newTodos, false)
console.log(res.data);
} catch (error) {
console.log(error.message);
}
}
Perceba que os dados que nós tinhamos compunham o objeto user
. Logo, precisamos atualizar todo o objeto, declarando que todoList
será a lista de tarefas antiga mais a nova tarefa. O segundo argumento de mutate, que definimos como false, é na verdade um objeto que guarda as opções da função, como a de revalidar os dados. Usar false
é declarar que não queremos os dados revalidados, apenas adicionados ao cache.
Lembro apenas que este mutate é o retornado do useCurrentUser, que utiliza, por sua vez, o mutate do useSWR com a key ‘/api/current’
. Se você quiser usar SWR para outras requests na sua aplicação, lembre-se de renomear o mutate de acordo com o caso, como fizemos com user, que antes era simplesmente data.
Removendo uma tarefa
Falta lidarmos apenas com um caso essencial. Vamos até a nossa API todo
para criar a lógica de remoção de itens.
Diremos o seguinte: se o método for DELETE, selecionaremos a lista da database. Com a lista obtida, vamos filtrá-la para remover o item e, depois, substituir a lista antiga pela lista filtrada:
if (req.method === "DELETE") {
const list = await prismadb.user.findUnique({
where: {
email: currentUser.email,
},
select: {
todoList: true,
},
});
const filteredList = list.todoList.filter(el => el != todo);
const response = await prismadb.user.update({
where: {
email: currentUser.email,
},
data: {
todoList: {
set: filteredList,
},
},
});
res.status(200).json("Tarefa removida com sucesso.");
}
Se você logar list
verá que é um objeto que contém a lista como propriedade, por isso usamos list.todoList
para manipular a lista com o método filter
.
Obviamente filtrar uma array de strings não é um bom método de exclusão de itens, já que duas tarefas idênticas seriam removidas e o usuário poderia querer remover apenas uma delas. Poderíamos criar, ao invés de uma simples array de strings, uma array de objetos em que cada tarefa possui um ID, e filtrar este ID ao invés da própria “todo”. Mas o método atual serve ao propósito de demonstração do artigo.
O importante é entender que, para remover um item de uma array, selecionamos a lista, modificamos o necessário e recolocamos a nova lista no lugar.
Agora, para deletar a tarefa, criaremos uma função na aplicação que, ao ser chamada, faz uma request do tipo DELETE à API, passando a tarefa como argumento:
async function removeTodo(todo) {
try {
const filteredList = user.todoList.filter(el => el != todo);
const newTodos = {...user, todoList: filteredList}
const res = await axios.delete("/api/todo", {
data: {todo: todo}
});
mutate(newTodos, false);
console.log(res.data);
} catch (error) {
console.log(error.message);
}
}
Mais uma vez a nossa função criará a lista atualizada e a colocará no cache para não precisar fazer outra request com SWR. Para chamar a função, criamos um botão e passamos todo
como argumento:
<ul>
{user.todoList.map((todo, index) => (
<li key={index}>
<p>{todo}</p>
<button onClick={() => removeTodo(todo)}>Remover</button>
</li>
))}
</ul>
Considerações finais
Pronto! É isso. Final anticlimático, né? Deixe-me melhorar.
Meus sinceros parabéns! A aplicação está criada e funcionando. Nosso usuário pode se registrar, logar, adicionar e remover tarefas. O que mais queremos? Talvez uma função para editar as tarefas? Outra para marcar tarefas como realizadas (movendo-as para a nossa doneList
)? Deixar a página bonita para ninguém querer filtrar os olhos para fora da cabeça? Isso tudo deixo com você.
Espero que você tenha compreendido meu propósito, que não era criar uma aplicação específica do início ao fim, mas usar o tempo que outros tutorais gastam com aspectos individuais de cada projeto para explicar o que eles não explicam. O que construímos aqui foi um esqueleto, e o que tentei fazer foi capacitá-lo a colocar nele a pele que quiser.
Se você ficou com alguma dúvida, tem alguma sugestão ou feedback sobre o tutorial, por favor, não hesite em entrar em contato.
Um abraço! E happy coding :)
Top comments (0)