Leodario.com

Leodario.com – Tudo sobre Tecnologia

Aula 77 – API JavaScript, Node.js e Express – TDD Approach – Test-Driven Development

Imagem destacada da aula de API

Introdução

Olá, futuro arquiteto de software! Sejam bem-vindos à Aula 77, onde mergulharemos em uma abordagem de desenvolvimento que transformará a maneira como você constrói suas APIs: o Test-Driven Development (TDD). Prepare-se para uma experiência prática e reveladora, onde a qualidade e a confiança se tornam pilares do seu código.

Imagine-se construindo uma ponte colossal sobre um rio caudaloso. Um engenheiro experiente não começaria a lançar vigas e concreto aleatoriamente. Ele primeiro desenharia plantas detalhadas, faria cálculos exaustivos para cada segmento da ponte e, crucialmente, testaria a resistência de cada parte — a fundação, cada pilar, cada treliça — antes mesmo de iniciar a construção definitiva. Ele quer ter certeza de que cada pequeno componente suportará a carga esperada. Esta é a essência do TDD no desenvolvimento de software. Nós escrevemos o teste primeiro, para garantir que sabemos o que estamos construindo e que cada peça funcional realmente opere como planejado.

Esta metodologia é estratégica para APIs modernas porque viabiliza a criação de sistemas robustos, de fácil manutenção e com documentação implícita através dos testes. Em um cenário de microsserviços e integrações complexas, ter a certeza de que cada endpoint funciona conforme o esperado, mesmo após inúmeras alterações, é de valor inestimável. Garante a resiliência do seu sistema e minimiza surpresas desagradáveis em produção.

Nesta aula, desenvolveremos um pequeno serviço de gerenciamento de produtos utilizando Node.js e Express. Mais especificamente, vamos implementar a funcionalidade de criar um novo produto (POST /products) seguindo estritamente o fluxo TDD. Você verá como cada teste falhando nos guiará para a implementação correta do código, culminando em um endpoint funcional e testado.

Dentro do vasto ecossistema Node.js/Express, o TDD se encaixa perfeitamente, permitindo que desenvolvedores construam APIs de forma mais modular e confiável. Utilizaremos frameworks de teste populares como o Mocha e o Chai, que se integram de maneira fluida com suas aplicações Express.

Conceito Fundamental

O Test-Driven Development (TDD) é uma disciplina de desenvolvimento de software onde os testes são escritos antes do código de produção. Não é apenas uma forma de testar, mas sim uma metodologia de design que orienta a sua programação. O TDD segue um ciclo iterativo e bem definido, conhecido como “Red-Green-Refactor”:

  • Red (Vermelho): Escreva um pequeno teste automatizado para uma nova funcionalidade ou uma correção de bug. Este teste deve falhar. Por que falhar? Porque a funcionalidade ainda não foi implementada ou o bug ainda não foi corrigido. Esta falha valida que o teste está de fato testando o que você espera e que não há uma implementação oculta.
  • Green (Verde): Escreva a menor quantidade de código de produção necessária para que o teste recém-escrito passe. O objetivo aqui é apenas fazer o teste passar, sem se preocupar excessivamente com a elegância do código ou otimizações.
  • Refactor (Refatorar): Uma vez que o teste esteja passando (verde), você pode refatorar o código. Isso significa melhorar a estrutura, legibilidade e eficiência do código, sem alterar seu comportamento. Você tem a segurança de saber que, se algo quebrar durante a refatoração, seu teste (que está verde) ficará vermelho novamente, alertando-o imediatamente.

Este ciclo se repete continuamente, construindo o software em pequenos incrementos e com uma base de testes robusta.

A terminologia correta da indústria é vital. Usamos testes de unidade para verificar componentes individuais isoladamente (uma função, um método de classe), testes de integração para verificar a interação entre diferentes partes do sistema (um controlador com um serviço, um serviço com um banco de dados), e testes end-to-end (E2E) para simular o fluxo completo de um usuário. Dentro dos testes, você encontrará termos como assertions (as verificações que seu teste faz, por exemplo, expect(resultado).to.equal(esperado)), mocks e stubs (objetos simulados que substituem dependências reais para isolar a parte sendo testada, permitindo controlar seu comportamento e verificar interações).

Casos de uso reais em produção para TDD incluem o desenvolvimento de microsserviços, onde a garantia de que cada serviço se comporta como um contrato é fundamental. Em sistemas com lógica de negócios complexa, o TDD ajuda a mapear e validar cada regra, prevenindo regressões. Projetos de longa duração se beneficiam enormemente, pois a suíte de testes atua como uma rede de segurança valiosa, permitindo que novas funcionalidades sejam adicionadas ou refatorações extensas sejam realizadas com confiança.

O TDD se integra com outras tecnologias de forma orgânica. É um pilar para a Integração Contínua (CI), onde cada push de código para o repositório dispara automaticamente a execução de todos os testes, garantindo que a base de código esteja sempre em um estado funcional. Isso, por sua vez, alimenta a Entrega Contínua (CD), possibilitando releases mais frequentes e com menos riscos.

Entre as vantagens do TDD, destacam-se:

  • Qualidade do Código Superior: Os testes escritos primeiro forçam um design mais modular e coeso.
  • Menos Bugs: Problemas são identificados e corrigidos mais cedo no ciclo de desenvolvimento.
  • Documentação Viva: Os testes servem como exemplos claros de como o código deve ser usado e como se comporta.
  • Confiança na Refatoração: A suíte de testes garante que mudanças não quebrem funcionalidades existentes.
  • Velocidade a Longo Prazo: Embora pareça mais lento no início, a redução de bugs e a facilidade de manutenção aceleram o desenvolvimento no médio e longo prazo.

Contudo, existem desvantagens:

  • Curva de Aprendizagem: Requer uma mudança de mentalidade e prática.
  • Tempo Inicial Percebido: Pode parecer que demanda mais tempo no início do projeto.
  • Testes Ruins: Testes mal escritos (muito acoplados, difíceis de manter) podem se tornar um fardo.

O balanço entre vantagens e desvantagens pende fortemente para o lado positivo, especialmente em equipes e projetos maduros que visam robustez e escalabilidade.

Implementação Prática

Vamos aplicar o TDD para construir a funcionalidade de criação de produtos em uma API Node.js com Express. Nossa API terá um serviço (lógica de negócio) e um controlador (interação com o Express).

Primeiro, vamos configurar nosso ambiente. Crie um novo diretório e inicialize um projeto Node.js:

mkdir tdd-product-api
cd tdd-product-api
npm init -y

Agora, instale as dependências essenciais: Express para o servidor, Mocha para o executor de testes e Chai para as asserções.

npm install express
npm install --save-dev mocha chai supertest

Usaremos também o supertest para facilitar os testes de integração HTTP.

Modifique seu package.json para incluir o script de teste:

{
  "name": "tdd-product-api",
  "version": "1.0.0",
  "description": "",
  "main": "src/server.js",
  "scripts": {
    "start": "node src/server.js",
    "test": "mocha --recursive tests/*/.test.js --timeout 5000",
    "dev": "nodemon src/server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.19.2"
  },
  "devDependencies": {
    "chai": "^4.4.1",
    "mocha": "^10.4.0",
    "supertest": "^7.0.0",
    "nodemon": "^3.1.0"
  }
}

Instale o nodemon para desenvolvimento:

npm install --save-dev nodemon

Vamos estruturar nosso projeto para seguir as melhores práticas:

.
├── src/
│   ├── app.js
│   ├── server.js
│   ├── services/
│   │   └── productService.js
│   ├── controllers/
│   │   └── productController.js
│   └── routes/
│       └── productRoutes.js
└── tests/
    ├── unit/
    │   └── productService.test.js
    └── integration/
        └── productIntegration.test.js

Passo 1: Red – Escrever o primeiro teste de unidade (que falha)

Vamos começar testando a lógica de negócio principal: criar um produto. Crie o arquivo tests/unit/productService.test.js:

// tests/unit/productService.test.js
const chai = require('chai');
const expect = chai.expect;
const productService = require('../../src/services/productService'); // O serviço ainda não existe!

describe('Product Service Unit Tests', () => { // Limpa a "base de dados" em memória antes de cada teste para garantir isolamento beforeEach(() => { productService.clearProducts(); // Esta função ainda não existe! });

it('deve criar um novo produto com um nome e preço válidos', () => { // Cenário: Tentando criar um produto válido const productData = { name: 'Teclado Mecânico', price: 150.00 }; const newProduct = productService.createProduct(productData); // O método ainda não existe!

// Asserção: Verifique se o produto foi criado corretamente expect(newProduct).to.be.an('object'); expect(newProduct).to.have.property('id'); // Deve ter um ID gerado expect(newProduct.name).to.equal('Teclado Mecânico'); expect(newProduct.price).to.equal(150.00); expect(productService.getAllProducts()).to.have.lengthOf(1); // Deve haver um produto na lista });

it('não deve criar um produto sem nome', () => { // Cenário: Tentando criar um produto sem nome const productData = { price: 100.00 }; // Esperamos que a criação de um produto inválido lance um erro expect(() => productService.createProduct(productData)).to.throw('O nome do produto é obrigatório.'); expect(productService.getAllProducts()).to.have.lengthOf(0); // Nenhum produto deve ser adicionado });

it('não deve criar um produto com preço inválido (negativo ou zero)', () => { // Cenário: Tentando criar um produto com preço negativo const productData = { name: 'Mouse Gamer', price: -50.00 }; expect(() => productService.createProduct(productData)).to.throw('O preço do produto deve ser um número positivo.'); expect(productService.getAllProducts()).to.have.lengthOf(0); // Nenhum produto deve ser adicionado

// Cenário: Tentando criar um produto com preço zero const productDataZero = { name: 'Mouse Gamer', price: 0 }; expect(() => productService.createProduct(productDataZero)).to.throw('O preço do produto deve ser um número positivo.'); expect(productService.getAllProducts()).to.have.lengthOf(0); // Nenhum produto deve ser adicionado }); });

Rode os testes:

npm test

Você verá um erro “Cannot find module ‘../../src/services/productService’”. Isso é VERMELHO! Ótimo, o teste está falhando como esperado.

Passo 2: Green – Escrever o código de produção mínimo

Crie o arquivo src/services/productService.js. Nosso “banco de dados” será um array em memória para simplificar a aula, tornando-o compatível com HostGator Plano M sem exigir configurações complexas de DB.

// src/services/productService.js
let products = []; // Nosso "banco de dados" em memória
let nextProductId = 1; // Para simular IDs únicos

/* @function createProduct @description Cria um novo produto após validação. @param {object} productData - Dados do produto (name, price). @returns {object} O produto recém-criado. @throws {Error} Se os dados do produto forem inválidos. / function createProduct(productData) { // Validação de entrada robusta if (!productData.name || typeof productData.name !== 'string' || productData.name.trim() === '') { console.error('Erro de validação: Nome do produto é inválido.'); throw new Error('O nome do produto é obrigatório.'); } if (typeof productData.price !== 'number' || productData.price <= 0) { console.error('Erro de validação: Preço do produto é inválido.'); throw new Error('O preço do produto deve ser um número positivo.'); }

const newProduct = { id: nextProductId++, name: productData.name, price: productData.price }; products.push(newProduct); console.log(Produto criado: ${JSON.stringify(newProduct)}); // Logging profissional básico return newProduct; }

/ @function getAllProducts @description Retorna todos os produtos armazenados. @returns {Array} Lista de todos os produtos. / function getAllProducts() { console.log('Buscando todos os produtos...'); return [...products]; // Retorna uma cópia para evitar modificações externas diretas }

/ @function clearProducts @description Limpa a lista de produtos (para uso em testes). / function clearProducts() { products = []; nextProductId = 1; console.log('Lista de produtos limpa.'); }

module.exports = { createProduct, getAllProducts, clearProducts };

Rode os testes novamente:

npm test

Todos os testes de unidade devem estar passando! Isso é VERDE!

Passo 3: Refactor – Melhorar o código (se necessário)

Neste ponto, o código está relativamente simples e claro. Para um exemplo mais complexo, poderíamos extrair a lógica de validação para um módulo separado, ou talvez utilizar uma biblioteca de validação como Joi ou Zod. No entanto, para a nossa aula, a validação inline é suficiente e clara. A refatoração aqui seria mais sobre organização e nomenclatura, que já estão razoáveis.

Implementação do Controlador e Rotas (TDD de Integração)

Agora vamos integrar este serviço em um controlador e rota. Primeiro, o teste de integração (Red):

Crie o arquivo tests/integration/productIntegration.test.js:

// tests/integration/productIntegration.test.js
const request = require('supertest');
const chai = require('chai');
const expect = chai.expect;
const app = require('../../src/app'); // Nossa aplicação Express ainda não existe!
const productService = require('../../src/services/productService');

describe('Product API Integration Tests', () => { // Limpa os produtos antes de cada teste de integração beforeEach(() => { productService.clearProducts(); });

it('POST /products deve criar um produto com sucesso (status 201)', async () => { // Cenário: Enviar dados válidos para criar um produto const productData = { name: 'Monitor Ultrawide', price: 799.99 }; const res = await request(app) // Supertest faz a requisição HTTP .post('/api/products') // Rota que ainda será criada .send(productData) .expect(201); // Espera-se status 201 Created

// Asserções: Verificar a resposta e o estado do sistema expect(res.body).to.be.an('object'); expect(res.body).to.have.property('id'); expect(res.body.name).to.equal('Monitor Ultrawide'); expect(res.body.price).to.equal(799.99); expect(productService.getAllProducts()).to.have.lengthOf(1); });

it('POST /products deve retornar 400 se o nome estiver faltando', async () => { // Cenário: Enviar dados de produto sem nome const productData = { price: 250.00 }; const res = await request(app) .post('/api/products') .send(productData) .expect(400); // Espera-se status 400 Bad Request

// Asserções: Verificar a mensagem de erro expect(res.body).to.have.property('message', 'O nome do produto é obrigatório.'); expect(productService.getAllProducts()).to.have.lengthOf(0); });

it('POST /products deve retornar 400 se o preço for inválido', async () => { // Cenário: Enviar dados de produto com preço inválido const productData = { name: 'Cadeira Gamer', price: -100.00 }; const res = await request(app) .post('/api/products') .send(productData) .expect(400);

expect(res.body).to.have.property('message', 'O preço do produto deve ser um número positivo.'); expect(productService.getAllProducts()).to.have.lengthOf(0); }); });

Rode os testes. Eles falharão com “Cannot find module ‘../../src/app’”. Isso é VERMELHO!

Green – Implementar Controlador, Rotas e App Express

Crie o arquivo src/controllers/productController.js:

// src/controllers/productController.js
const productService = require('../services/productService');

/* @function createProduct @description Manipula a requisição HTTP para criar um novo produto. @param {object} req - Objeto de requisição do Express. @param {object} res - Objeto de resposta do Express. / async function createProduct(req, res) { try { const { name, price } = req.body; // Extrai dados do corpo da requisição const newProduct = productService.createProduct({ name, price }); // Chama o serviço res.status(201).json(newProduct); // Resposta de sucesso (201 Created) console.log(Requisição POST /api/products bem-sucedida. Produto ID: ${newProduct.id}); } catch (error) { // Error handling extraordinário: captura erros do serviço console.error(Erro ao criar produto: ${error.message}); // Mapeia o erro para um status HTTP apropriado if (error.message.includes('obrigatório') || error.message.includes('positivo')) { return res.status(400).json({ message: error.message }); // Bad Request } res.status(500).json({ message: 'Erro interno do servidor ao criar produto.' }); // Erro genérico do servidor } }

// Futuras implementações: // async function getProductById(req, res) { ... } // async function getAllProducts(req, res) { ... }

module.exports = { createProduct };

Crie o arquivo src/routes/productRoutes.js:

// src/routes/productRoutes.js
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');

// Define a rota POST para criar um novo produto router.post('/', productController.createProduct);

// Futuras rotas: // router.get('/:id', productController.getProductById); // router.get('/', productController.getAllProducts);

module.exports = router;

Crie o arquivo src/app.js, que é a base da nossa aplicação Express:

// src/app.js
const express = require('express');
const productRoutes = require('./routes/productRoutes');

const app = express();

// Middleware para parsear JSON no corpo das requisições app.use(express.json());

// Rota base para a API de produtos app.use('/api/products', productRoutes);

// Middleware de tratamento de erros global (melhor prática enterprise) app.use((err, req, res, next) => { console.error(Erro inesperado: ${err.stack}); res.status(500).json({ message: 'Ocorreu um erro interno no servidor.' }); });

module.exports = app; // Exporta a instância do app para ser usada nos testes e no servidor

E finalmente, o arquivo src/server.js para iniciar o servidor:

// src/server.js
const app = require('./app');

// Configuração para HostGator Plano M ou ambiente de produção: // Use a porta fornecida pelo ambiente (ex: process.env.PORT) ou a porta 3000 como fallback. const PORT = process.env.PORT || 3000;

app.listen(PORT, () => { console.log(Servidor rodando na porta ${PORT}); console.log('API de produtos pronta para receber requisições.'); });

// Em ambientes de produção HostGator (com Passenger), o app.js geralmente é o entry point. // Este server.js seria mais para testes locais ou outros ambientes. // A compatibilidade com HostGator Plano M é garantida pela ausência de dependências complexas // e uso de porta configurável.

Rode os testes novamente:

npm test

Todos os testes de integração devem estar passando! VERDE!

Refactor – Melhorar o código

Neste ponto, podemos considerar algumas melhorias:

  • Logging: Integrar bibliotecas como Winston ou Morgan para logging mais robusto e configurável, em vez de console.log.
  • Validação: Mover a validação de productData para um middleware de validação ou usar uma biblioteca como Joi ou Zod nas rotas.
  • Padronização de Erros: Criar classes de erro personalizadas (ValidationError, NotFoundError) para um tratamento de erros mais descritivo e padronizado em toda a API.
  • Documentação: Gerar documentação da API com Swagger/OpenAPI.

Estas são melhorias que você faria com a segurança dos testes existentes.

Exercício Hands-On

Agora é sua vez de aplicar o TDD!

Desafio Prático

Utilizando o mesmo projeto e a abordagem TDD, implemente a funcionalidade de obter todos os produtos na sua API. Isso significa:

    • Crie um novo teste de integração (Red) para o endpoint GET /api/products que espera retornar uma lista vazia se nenhum produto foi adicionado e uma lista com produtos se eles foram criados anteriormente.
    • Faça o teste passar (Green) implementando o método getAllProducts no productController.js e adicionando a rota GET / em productRoutes.js.
    • Refatore (Refactor) se sentir necessidade.

Solução Detalhada Passo a Passo

1. Red – Escrever o teste de integração para GET /api/products

Adicione os seguintes testes ao final do arquivo tests/integration/productIntegration.test.js:

// ... (código anterior)

it('GET /products deve retornar uma lista vazia quando não há produtos', async () => { // Cenário: Nenhuma produto adicionado ainda const res = await request(app) .get('/api/products') // Rota que ainda será criada .expect(200); // Espera-se status 200 OK

// Asserções: A lista deve ser um array vazio expect(res.body).to.be.an('array').that.is.empty; expect(productService.getAllProducts()).to.have.lengthOf(0); });

it('GET /products deve retornar todos os produtos existentes', async () => { // Cenário: Criar alguns produtos primeiro productService.createProduct({ name: 'Fone de Ouvido', price: 99.90 }); productService.createProduct({ name: 'Webcam HD', price: 120.00 });

const res = await request(app) .get('/api/products') .expect(200);

// Asserções: A lista deve conter os dois produtos criados expect(res.body).to.be.an('array').that.has.lengthOf(2); expect(res.body[0].name).to.equal('Fone de Ouvido'); expect(res.body[1].name).to.equal('Webcam HD'); });

// ... (fim do arquivo)

Rode npm test. Os novos testes devem falhar, provavelmente com um erro 404 (Not Found) ou 500 (Internal Server Error), pois a rota ainda não existe ou o controlador não está implementado. VERMELHO!

2. Green – Implementar a funcionalidade para fazer o teste passar

a. No src/controllers/productController.js, adicione a função getAllProducts:

// src/controllers/productController.js
const productService = require('../services/productService');

// ... (função createProduct anterior)

/* @function getAllProducts @description Manipula a requisição HTTP para obter todos os produtos. @param {object} req - Objeto de requisição do Express. @param {object} res - Objeto de resposta do Express. / async function getAllProducts(req, res) { try { const products = productService.getAllProducts(); // Chama o serviço para obter os produtos res.status(200).json(products); // Responde com status 200 OK e a lista de produtos console.log('Requisição GET /api/products bem-sucedida. Total de produtos:', products.length); } catch (error) { console.error(Erro ao obter produtos: ${error.message}); res.status(500).json({ message: 'Erro interno do servidor ao buscar produtos.' }); } }

module.exports = { createProduct, getAllProducts // Exporta a nova função };

b. No src/routes/productRoutes.js, adicione a rota GET:

// src/routes/productRoutes.js
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');

router.post('/', productController.createProduct); router.get('/', productController.getAllProducts); // Adiciona a nova rota GET

module.exports = router;

Rode npm test novamente. Todos os testes, incluindo os novos, devem passar! VERDE!

3. Refactor – Melhorar o código

Neste caso, o código é simples e direto. Não há uma refatoração óbvia necessária de imediato. A estrutura de try/catch já garante um tratamento de erro básico. Você poderia considerar a padronização das mensagens de log e erro para toda a aplicação.

Como Testar e Validar o Resultado

Para validar seu resultado, execute npm test. Se todos os testes passarem, sua implementação está correta e funcional conforme as expectativas definidas nos testes.

Você também pode iniciar o servidor com npm start e testar manualmente usando ferramentas como Postman, Insomnia ou curl:

# Crie um produto (para ter algo para buscar)
curl -X POST -H "Content-Type: application/json" -d '{"name":"Smartwatch X","price":300.00}' http://localhost:3000/api/products

Busque todos os produtos

📚 Informações da Aula

Curso: API Completo - Node.js & Express

Tempo estimado: 25 minutos

Pré-requisitos: JavaScript básico

curl http://localhost:3000/api/products

A primeira requisição deve retornar o produto criado com status 201. A segunda deve retornar um array JSON contendo o Smartwatch X.

Troubleshooting dos Erros Mais Comuns

    • Cannot find module '...': Verifique os caminhos nos seus require(). Erros de caminho são muito comuns.
    • Testes falhando com 404 (Not Found) para rotas que você adicionou:
      • Verifique se a rota está corretamente definida no productRoutes.js (ex: router.get('/')).
      • Certifique-se de que productRoutes está sendo usado em app.js (ex: app.use('/api/products', productRoutes)).
      • Confira se a URL no seu teste (/api/products) corresponde à rota esperada.
    • Testes falhando com 500 (Internal Server Error) inesperado:
      • Examine os logs do seu servidor (ou do terminal onde você rodou npm test) para ver a stack trace do erro. Isso indicará onde o erro original ocorreu.
      • Pode ser um erro na lógica do controlador ou do serviço, ou um problema com o tratamento de exceções.
    • Testes de asserção falhando (ex: expected ... to equal ...):
      • Revise a lógica de retorno do seu controlador ou serviço. O que você está retornando é o que o teste espera?
      • Verifique se os dados de entrada no teste estão corretos.

Próximos Passos Sugeridos

Para aprofundar seus conhecimentos em TDD e APIs:

    • Adicione mais funcionalidades com TDD: Implemente GET /api/products/:id (buscar por ID), PUT /api/products/:id (atualizar) e DELETE /api/products/:id (remover).
    • Aprofunde em Mocks e Stubs: Aprenda a usar ferramentas como Sinon.js para isolar ainda mais seus testes de unidade, simulando o comportamento de dependências externas (como um banco de dados real ou serviços externos).
    • Autenticação e Autorização: Comece a explorar como implementar e testar autenticação e autorização em suas APIs usando TDD.
    • Integração Contínua (CI): Configure seu projeto com uma ferramenta de CI (como GitHub Actions, GitLab CI ou Jenkins) para que seus testes rodem automaticamente a cada push.
    • Bancos de Dados Reais: Substitua o array em memória por um banco de dados real (MongoDB com Mongoose, PostgreSQL com Sequelize/Prisma) e adapte seus testes. Lembre-se de ter um banco de dados de teste separado para evitar conflitos com dados de desenvolvimento.
    • Melhore o Logging e Validação: Implemente Winston para logging e Joi ou Zod para validação de esquemas de entrada. Teste essas implementações também!

A prática contínua é a chave para dominar o TDD. Mantenha seus testes limpos, focados e rápidos, e você construirá APIs que são um verdadeiro deleite para desenvolver e manter.

🚀 Pronto para a próxima aula?

Continue sua jornada no desenvolvimento de APIs e domine Node.js & Express!

📚 Ver todas as aulas