Crie uma API CRUD com MongoDB, Typescript, Express, Prisma e Zod
Onyedikachi Kindness Eni9 min read • Published Sep 04, 2024 • Updated Sep 04, 2024
APLICATIVO COMPLETO
Avalie esse Tutorial
As tecnologias tradicionais geralmente têm dificuldades para acompanhar as demandas de escalabilidade, manutenção e ciclos rápidos de desenvolvimento. Esses sistemas são atormentados por arquiteturas monolíticas, bases de código difíceis de manter e falta de ferramentas modernas, o que gera ineficiências e aumenta os custos de desenvolvimento.
Entre na pilha moderna: TypeScript, Express, Prisma, MongoDB e Zod. Esta combinação poderosa oferece uma solução robusta, segura para tipos e escalável para a construção de aplicativos da web. O TypeScript aprimora o JavaScript com tipagem estática, reduzindo erros de tempo de execução e melhorando a produtividade do desenvolvedor. Express, uma estrutura da web minimalista, simplifica a criação de APIs com seus recursos de roteamento e middleware leves, mas poderosos. Prisma, um ORM de última geração, fornece uma interface de tipo seguro para interagir com bancos de dados, preenchendo a lacuna entre a lógica do aplicativo e as operações do banco de dados. MongoDB Atlas, um banco de dados e plataforma de dados NoSQL líder, se destaca no tratamento de grandes volumes de dados não estruturados e dimensionamento horizontal. Por fim, o Zod garante a integridade dos dados com sua declaração e validação de esquema TypeScript.
Usando essa pilha moderna, criaremos uma API básica de gerenciamento de usuários que executará as seguintes operações CRUD:
- Criar contas de usuário
- Ler perfis de usuário
- Atualizar informações do usuário
- Excluir contas de usuário
O Express será usado para construir o servidor web, Prisma o ORM, MongoDB como banco de dados e MongoDB para validar as cargas úteis.
- Conhecimento prático do Typescript
- Familiaridade com o Express
- MongoDB Atlas cluster
- Editor de código — como o VS Code
- Node.js LTS mais recente instalado no seu computador
Crie um diretório de projeto chamado
CRUD-API
e navegue até ele.1 mkdir CRUD-API 2 cd CRUD-API
Em seguida, inicialize um novo projeto e instale o Typescript e o Prisma:
1 npm init -y 2 npm install prisma typescript ts-node @types/node --save-dev
O comando anterior cria um arquivo
package.json
com uma configuração Typescript:Em seguida, inicialize o TypeScript:
1 npx tsc --init
1 npx prisma
Execute o seguinte comando para configurar seu projeto Prisma ORM:
1 npx prisma init
O comando anterior realiza as seguintes ações:
- Cria um novo diretório Prisma com um arquivo schema.prisma
- Cria um .env arquivo, onde será definida a variável de ambiente utilizada para a conexão com o banco de dados
Execute o comando a seguir para instalar o pacote @prisma/cliente, o que nos permitirá usar o Prisma Client.
1 npm install @prisma/client express zod
No comando anterior, instalamos o pacote
@prisma/client
, Expresse zod. O pacote@prisma/client
nos permitirá usar o Prisma Client para acesso ao banco de dados.Por fim, execute o seguinte comando:
1 npm install @types/express nodemon --save-dev
Depois de instalar as dependências, seu
package.json
arquivo deve ficar assim:1 { 2 "name": "crud-api", 3 "version": "1.0.0", 4 "description": "", 5 "main": "index.js", 6 "scripts": { 7 "test": "echo \"Error: no test specified\" && exit 1" 8 }, 9 "keywords": [], 10 "author": "", 11 "license": "ISC", 12 "devDependencies": { 13 "@types/express": "^4.17.21", 14 "@types/node": "^20.12.10", 15 "nodemon": "^3.1.0", 16 "prisma": "^5.13.0", 17 "ts-node": "^10.9.2", 18 "typescript": "^5.4.5" 19 }, 20 "dependencies": { 21 "@prisma/client": "^5.13.0", 22 "express": "^4.19.2", 23 "zod": "^3.23.7" 24 } 25 }
A estrutura de árvore do projeto deve ficar assim:
1 ├── CRUD-API 2 │ ├── node_modules 3 │ ├── Prisma 4 │ │ └── Schema.prisma 5 │ ├── src 6 │ ├── .gitignore 7 │ ├── package.json 8 │ ├── package-lock.json 9 │ └── tsconfig.json
No diretório raiz do projeto, crie um subdiretório
src
. O diretóriosrc
incluirá o código fonte da API.Em seguida, no diretório src, crie um arquivo
app.ts
e insira o seguinte código:1 import Express from 'express'; 2 3 // Initiate express 4 const app = Express(); 5 6 app.use(Express.json()); 7 8 // Setup “hello world” endpoint 9 10 const port = process.env.PORT || 3000; 11 12 app.get('*', (req, res) => res.send('Hello World!')); 13 14 // Start the express server on the relevant port 15 app.listen(port, () => { 16 console.log(`server is running on ${port}`); 17 });
Para iniciar nosso servidor, temos que adicionar um novo script ao nosso arquivo package.json. Adicione a seguinte linha à propriedade scripts do arquivo package.json:
1 "dev": "nodemon --watch ./src --ext ts --exec 'ts-node ./src/app.ts'"
No código anterior, estamos usando o comando nodemon. O nodemon é uma ferramenta soberba para aplicativos Node.js. Ele pode reiniciar seu servidor quando a origem muda.
Digite o comando abaixo para iniciar o servidor:
1 npm run dev
Agora, quando você for ao seu navegador e inserir
http://localhost:3000
, você verá o texto Hello World!
da nossa função de chamada de resposta Express. Para armazenar nossos dados, usaremos MongoDB Atlas. Uma URL de conexão do banco de dados é necessária para conectar o banco de dados via Prisma. Siga o guia para configurar um cluster MongoDB Atlas e obter um URL de conexão.
A seguir, no arquivo
.env
, adicione o URL de conexão como uma variável de ambiente:1 DATABASE_URL="mongodb+srv://<username>:<password>@<cluster-name>.xxxxx.mongodb.net/"
No arquivo
prisma.schema
, insira o seguinte código:1 generator client { 2 provider = "prisma-client-js" 3 } 4 5 datasource db { 6 provider = "mongodb" 7 url = env("DATABASE_URL") 8 }
O código anterior fornece os dados necessários para que o Prisma se conecte ao nosso banco de dados. No bloco da fonte de dados, o valor passado para o provedor representa o tipo de banco de dados que está sendo usado (MongoDB, nesse caso), enquanto o valor de URL é a URL de conexão do banco de dados.
Como estamos criando uma API básica de gerenciamento de usuários, apenas uma única collection será necessária.
O modelo de usuário definirá os detalhes do usuário a serem armazenados no banco de dados, com os seguintes atributos:
- nome: string, campo necessário para armazenar o nome do usuário
- e-mail: string, campo obrigatório para armazenar o e-mail do usuário
- phoneNumber: string, campo obrigatório para armazenar o número de telefone do usuário
- gênero: string, campo obrigatório para armazenar o gênero do usuário
Para habilitar e lidar com as operações CRUD do usuário no banco de dados, implementaremos os seguintes endpoints de API.
1 Operation API route HTTP method 2 Create User /api/users POST 3 Get Users /api/users GET 4 Fetch a User /api/users:userId GET 5 Update a User/api/users:userId PATCH 6 Delete a User/api/users:userId DELETE
Agora, vamos criar o esquema da API.
No arquivo
prisma/schema.prisma
, adicione o seguinte código:1 model User { 2 id String @id @default(auto()) @map("_id") @db.ObjectId 3 name String 4 email String 5 phoneNumber String 6 gender String 7 }
Em um banco de dados relacional tradicional, um modelo criará uma tabela, mas como estamos usando o MongoDB, nosso modelo cria uma collection. Observe que o campo id é mapeado com o gravador
@map("_id")
porque _id
é o padrão fornecido pelo MongoDB.Agora, vamos gerar o Prisma Client executando o seguinte comando:
1 npx prisma generate
O Prisma Client nos dá acesso com segurança de tipo ao nosso banco de dados.
Em seguida, implementaremos nossos comandos de rota que utilizarão o Prisma Client para executar operações no banco de dados.
Basicamente, os roteadores e controladores permitem o tratamento estruturado de solicitações e respostas em um aplicativo. Os roteadores mapeiam as solicitações HTTP de entrada para endpoints específicos, enquanto os drivers recebem as solicitações dos roteadores, processam qualquer lógica de negócios, interagem com o banco de dados e retornam as respostas apropriadas ao cliente.
Agora, vamos criar as rotas e drivers.
No
src
diretório, crie um client.ts
arquivo e insira o seguinte código:1 import { PrismaClient } from '@prisma/client'; 2 3 const prisma = new PrismaClient(); 4 5 export default prisma;
No código anterior, criamos uma instância do Prisma Client, que será usada para interação com o banco de dados.
Em seguida, no diretório
src
, crie os subdiretórios routes
e controllers
.1 📦 CRUD-API 2 ┣ 📂 src 3 ┃ ┣ 📂 routes 4 ┃ ┣ 📂 controllers 5 ┃ ┣ 📄 app.ts 6 ┃ ┣ 📄 client.ts
Por padrão, um controlador lida com as solicitações recebidas, processa-as e fornece uma resposta apropriada. Vamos implementar os comandos para nossa API.
No subdiretório de comandos, crie um arquivo
user.route.ts
.O createUser lida com solicitações POST para criar novos usuários.
Digite o seguinte código no arquivo
user.route.ts
.1 import { Request, Response } from "express"; 2 import prisma from "../client"; 3 4 // Creating a user 5 export async function createUser(req: Request, res: Response) { 6 try { 7 const user = await prisma.user.create({ 8 data: req.body, 9 }); 10 11 res.status(201).json({ 12 status: true, 13 message: "User Successfully Created", 14 data: user, 15 }); 16 } catch (error) { 17 res.status(500).json({ 18 status: false, 19 message: 'server error' 20 }); 21 } 22 }
O controlador getUser lida com solicitações GET para todos os usuários.
Digite o seguinte no arquivo
user.route.ts
.1 // Get all Users 2 export async function getUsers(req: Request, res: Response) { 3 const users = await prisma.user.findMany(); 4 5 res.json({ 6 status: true, 7 message: "Users Successfully fetched", 8 data: users, 9 }); 10 } 11 12 In the preceding code, the getUsers controller gets all users. 13 14 ### getUser controller 15 The getUser controller handles GET requests for a single user. 16 17 Enter the following in the `user.route.ts` file. 18 // Get a single user 19 export async function getUser(req: Request, res: Response) { 20 const { userid } = req.params; 21 const user = await prisma.user.findFirst({ 22 where: { 23 id: userid, 24 }, 25 }); 26 27 res.json({ 28 status: true, 29 message: "User Successfully fetched", 30 data: user, 31 }); 32 }
No código anterior, o controlador getUser obtém um único usuário.
Digite o seguinte no arquivo
user.route.ts
.1 // deleting a user 2 export async function deleteUser(req: Request, res: Response) { 3 const { userid } = req.params; 4 5 try { 6 const user = await prisma.user.findFirst({ 7 where: { 8 id: userid, 9 }, 10 }); 11 12 if (!user) { 13 return res.status(401).json({ 14 status: false, 15 message: 'User not found', 16 }); 17 } 18 await prisma.user.delete({ 19 where: { 20 id: userid, 21 }, 22 }), 23 res.json({ 24 status: true, 25 message: 'User Successfully deleted', 26 }); 27 } catch { 28 res.status(501).json({ 29 status: false, 30 message: 'server error', 31 }); 32 } 33 }
No código anterior, o controlador deleteUser exclui um usuário.
Digite o seguinte no arquivo
user.route.ts
.1 // updating a single user 2 export async function updateUser(req: Request, res: Response) { 3 try { 4 const { userid } = req.params; 5 6 const user = await prisma.user.findFirst({ 7 where: { 8 id: userid, 9 }, 10 }); 11 12 if (!user) { 13 return res.status(401).json({ 14 status: false, 15 message: 'User not found', 16 }); 17 } 18 19 const updatedUser = await prisma.user.update({ 20 where: { 21 id: userid, 22 }, 23 data: req.body, 24 }); 25 26 res.json({ 27 status: true, 28 message: 'User Successfully updated', 29 data: updatedUser, 30 }); 31 } catch (error) { 32 console.log(error); 33 res.status(500).json({ 34 status: false, 35 message: 'server error', 36 }); 37 } 38 }
No código anterior, o controlador updateUser atualiza um usuário.
Em seguida, vamos criar as rotas que utilizarão os drivers.
No subdiretório routes, crie um arquivo
index.ts
e user.routes.ts
.Em
index.ts
, insira o seguinte código:1 import { Router } from 'express'; 2 import userRoute from './user.route'; 3 4 // Index 5 const indexRoute = Router(); 6 7 indexRoute.get('', async (req, res) => { 8 res.json({ message: 'Welcome User' }); 9 }); 10 11 indexRoute.use('/users', userRoute); 12 13 export default indexRoute; 14 15 Next, in user.routes.ts enter the following code: 16 import { Router } from 'express'; 17 import { 18 createUser, 19 deleteUser, 20 getUser, 21 getUsers, 22 updateUser, 23 } from '../controllers/user.controller'; 24 25 // Users layout Route 26 const userRoute = Router(); 27 28 userRoute.post('', createUser); 29 userRoute.get('', getUsers); 30 userRoute.get('/:userid', getUser); 31 userRoute.delete('/:userid', deleteUser); 32 userRoute.patch('/:userid', updateUser); 33 34 export default userRoute;
A rota GET pode ser visualizada no navegador, mas para verificar as outras rotas, usaremos a ferramentaPOSTMAN.
Vamos criar um novo usuário por meio do método POST. Na barra de endereço no Postman, insira a URL de solicitação
http://localhost:3000/users
, insira a carga útil e clique no botão enviar. Receberemos de volta o JSON correto e também o status de 201 — confirmando que um recurso foi criado. Solicitação POST: criar um usuárioO método GET é usado para buscar todos os usuários. Ele não requer carga útil.
GET Request: Get all Users
No entanto, para buscar um único usuário, precisamos fornecer o ID do usuário. O ponto de extremidade será
http://localhost:3000/users/id
.GET Request: Get a single user
Para o método DELETE, será fornecido o id do usuário que está sendo excluído. Novamente, estamos usando o endpoint
http://localhost:3000/users/id
. DELETE: delete a user
Para o método PATCH também temos que fornecer um ID do usuário que está sendo atualizado. Usaremos o ponto de extremidade
http://localhost:3000/users/id
. PATCH: update a user
Observe que passamos um valor inválido james.com no campo de e-mail e ele é considerado válido. Esse não será o caso depois que implementarmos o middleware de validação.
Agora, vamos validar a carga útil das solicitações recebidas (POST, PATCH) para garantir que elas atendam ao formato de dados necessário, conforme especificado em nosso modelo — ou seja, um
e-mail
deve ser um endereçoe-mail
válido (não qualquer string).Crie um subdiretório schemas no diretório src e, em seguida, crie um arquivo
user.schema.ts
.Digite o seguinte código em
user.schema.ts
:1 import { z } from 'zod'; 2 3 export const createUserSchema = z 4 .object({ 5 name: z.string().min(1), 6 email: z.string().email(), 7 phoneNumber: z.string().regex(/^\d+$/), 8 gender: z.enum(['male', 'female', 'others']), 9 }) 10 .strict(); //strict prevents the schema from validating payloads with properties not in the schema 11 12 export const updateUserSchema = createUserSchema.partial(); //creates a partial schema from createUserSchema where all properties are optional
No código anterior, os seguintes esquemas foram criados:
- createUserSchema: valida a carga útil que chega ao controlador createUser por meio da solicitação POST.
- updateUserSchema: Valida a carga útil que chega ao controlador updateUser.
Agora, vamos criar um middleware de validação que usa o esquema para validar uma carga de entrada.
1 import { NextFunction, Request, Response } from 'express'; 2 import { ZodSchema } from 'zod'; 3 4 // Validation middleware 5 export const validateSchema = 6 (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => { 7 // parse request body 8 const { success, error } = schema.safeParse(req.body); 9 10 // handle non-compliant request body 11 if (!success) { 12 return res.status(401).json({ 13 status: false, 14 message: error.errors 15 .map((t) => `${t.path[0] ?? ''}: ${t.message}`) 16 .join(', '), 17 }); 18 } 19 20 next(); 21 };
No código anterior, o esquema Zod é usado para analisar o corpo da solicitação. Se a análise for bem-sucedida, a próxima função será chamada. Caso contrário, uma resposta JSON com um código de status 401 e uma mensagem de erro será retornada.
Agora, vamos modificar o arquivo
user.routes.ts
adicionando o middleware de validação às rotas createUser e updateUser.1 import { Router } from 'express'; 2 import { 3 createUser, 4 deleteUser, 5 getUser, 6 getUsers, 7 updateUser, 8 } from '../controllers/user.controller'; 9 import { validateSchema } from '../middlewares/validation.middleware'; 10 import { createUserSchema, updateUserSchema } from '../schemas/user.schema'; 11 12 // Users layout Route 13 const userRoute = Router(); 14 15 userRoute.post('', validateSchema(createUserSchema), createUser); 16 userRoute.get('', getUsers); 17 userRoute.get('/:userid', getUser); 18 userRoute.delete('/:userid', deleteUser); 19 userRoute.patch('/:userid', validateSchema(updateUserSchema), updateUser); 20 21 export default userRoute;
Em comparação com nossa implementação anterior (sem validação), o envio de uma carga inválida retornará um erro agora.
Vamos testar o método PATCH novamente:
PATCH: Invalid e-mail field
Desta vez, obtemos um erro indicando que o valor do campo de e-mail é inválido.
Agora, se usarmos um endereço de e-mail válido, os erros desaparecerão: PATCH: Campo de e-mail válido
Neste tutorial, criamos uma API CRUD simples com Express, MongoDB e Sod. Você aprenderam como criar um servidor com Express, configurar um esquema Prisma, validar cargas úteis com Sod e testar API com Postman. Sinta-se livre para clonar o projeto no GitHub, inscrever-se no MongoDB Atlase construir sobre esta base. Se você quiser continuar a conversa, junta-se a nós na Comunidade de desenvolvedores MongoDB.