Nesta série de artigos estamos explorando o uso do TypeScript, inicialmente fazendo alguns testes básicos e mais pro fim da segunda parte, iniciamos a construção de uma WebAPI com Express e TypeScript, sendo que apenas deixamos tudo configurado.
É nesta terceira parte que vamos de fato usar TypeScript com Express para construção de uma WebAPI de exemplo.
Atenção: este é um post intermediário, para alguém que já tem conhecimentos básicos em Node.js. Ele já vai direto ao ponto, para quem precisa de algo com mais didática, recomendo começar com esse.
#1 – Routes com TypeScript
Crie uma pasta routes no seu projeto Express e cria uma index.ts dentro dela, com o seguinte código.
1 2 3 4 5 6 7 8 9 10 11 |
import {Router,Request,Response} from 'express'; const router = Router(); router.get('/', (req: Request, res: Response) => { res.json({success: true}); }) export default router; |
Aqui estamos usando a sintaxe mais moderna para JavaScript, com import para dependências e export para exportação de objetos. Também usamos decomposição na importação, pois só precisamos de alguns componentes do pacote Express, como a função Router e os tipos Request e Response.
De resto, é uma rota bem simples, como já fizemos anteriormente, com a diferença da tipagem nos parâmetros do callback.
Lá no nosso app.ts, vamos modificar também, para usarmos este arquivo de rotas.
1 2 3 4 5 6 7 8 9 10 |
import express from 'express'; import indexRoutes from './routes/index'; const app = express(); app.use(indexRoutes); app.listen(process.env.PORT || 3000); |
Do jeito que está, esta rota já funciona, mas não faz nada que preste.
Então vamos adicionar uns models tipados na nossa api.
#2 – Models com TypeScript
Crie uma pasta models no seu projeto e dentro crie um arquivo customer.ts, que definirá o tipo de dado Customer da nossa web API. Via de regra, você pode fazer definição de tipos usando interfaces, como abaixo, ou com types, o resultado é o mesmo neste cenário simples, sendo a única diferença em cenários orientados à objetos, onde a interface possui a habilidade de herança.
1 2 3 4 5 6 7 |
export interface Customer{ id: number, name: string, birthDate: Date } |
O mesmo código acima, usando type, seria:
1 2 3 4 5 6 7 |
export type Customer = { id: number, name: string, birthDate: Date } |
Agora, para usar esse novo tipo que criamos, vamos importá-lo no nosso arquivo de rotas.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import {Router,Request,Response} from 'express'; import {Customer} from '../models/customer'; const customers : Customer[] = []; const router = Router(); router.get('/', (req: Request, res: Response) => { res.json(customers); }) export default router; |
Note que usei o novo tipo importado para criarmos um array in-memory fortemente tipado, como o TS strict nos manda fazer e estamos retornando o mesmo em nossa rota GET /. Você pode testar essa rota facilmente subindo a aplicação e acessando no navegador em localhost:3000/
Mas e se quisermos criar uma rota para salvar novos customers?
1 2 3 4 5 6 7 8 9 10 11 |
router.post('/', (req, res) => { const newCustomer : Customer = { id: parseInt(req.body.id), name: req.body.name, birthDate: new Date(Date.parse(req.body.birthDate)) }; customers.push(newCustomer); res.status(201).json(newCustomer); }) |
Aqui, declaramos uma constante newCustomer que é do tipo Customer, o que obriga o preenchimento dos seus 3 campos, que obteremos no corpo da requisição, afinal é um POST /.
Para que o body da requisição seja corretamente interpretado pelo Express, precisamos voltar ao nosso app.ts e usar o body-parser de JSON que vem nativo no Express.
1 2 3 4 5 6 7 8 9 10 |
import express from 'express'; import indexRoutes from './routes/index'; const app = express(); app.use(express.json()); app.use(indexRoutes); app.listen(process.env.PORT || 3000); |
Para testar rotas POST você vai precisar de alguma ferramenta, sendo que a que eu costumo usar a recomendo é a Postman, onde mostro no vídeo abaixo como usar.
#3 – Typecasting com Express
Você deve ter notado que o req.body e os req.params do Express não estão tipados, o que mantém a possibilidade de falhas na codificação.
Assim como fizemos anteriormente, podemos utilizar typecasting para converter objetos dinâmicos como o body e o params em tipos estáticos definidos por nós. Sim, por nós, porque não tem como existir um pacote @types para todas as possibilidades que você pode inventar na sua web API né…
No exemplo abaixo, criei um tipo para o body da requisição da nossa rota, o que pode ser útil para POST, PUT e PATCH, dependendo de como você está estruturando sua web API.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
type PostBody = { id: number, name: string, birthDate: string } router.post('/', (req: Request, res: Response) => { const body = req.body as PostBody; const newCustomer : Customer = { id: body.id, name: body.name, birthDate: new Date(Date.parse(body.birthDate)) }; customers.push(newCustomer); res.status(201).json(newCustomer); }) |
Novamente, tome cuidado com typecasting pois ele não faz milagre. Além disso, uma dúvida que pode surgir é: mas PostBody não é a mesma coisa que a interface Customer? Na verdade não, repare que o campo birthDate do PostBody é string, pois não tem como enviarmos um objeto de data em uma requisição POST, temos de enviá-lo ou como string ou como timestamp (number). Mais tarde, usamos esta string que estamos recebendo para converter para o formato Date esperado pelo Customer. Além disso, imagine que em determinados endpoints, o conteúdo do body não vai ser exatamente igual em termos de campos, então cada um pode ter um type completamente diferente de Customer ou contendo apenas partes dele (PatchBody, etc).
#4 – Compilando
Uma coisa que pode estar incomodando você é que você vai estar o tempo todo programando em arquivos TS e compilando eles em JS, criando não apenas o trabalho de compilação, mas também a bagunça de arquivos praticamente duplicados por todo o projeto, certo?
Aqui vão duas boas práticas de compilação.
A primeira é abrir o seu tsconfig e alterar o diretório padrão onde os arquivos compilados serão armazenados.
1 2 3 |
"outDir": "./dist/", |
A sugestão acima é que os arquivos JS sejam compilados e armazenados em uma pasta dist na raiz do projeto.
Agora, para compilar tudo que programamos nessa Web API e ver se essa config surtiu efeito, apenas navegue até a raiz do projeto e rode o comando tsc nela, sem nenhum parâmetro adicional.
Isso irá compilar todos os arquivos TS do seu projeto e colocar os equivalentes JS, com a mesma estrutura de pastas original, dentro da pasta dist.
É essa pasta dist que contém a sua aplicação compilada em JS e pronta para execução. Recomendo que coloque ela no seu .gitignore porque não há necessidade de ter ela versionada, pois toda vez que rodar tsc ela será atualizada.
E por fim, também recomendo que configure o script de start corretamente no seu packages.json.
1 2 3 |
"start": "node ./dist/app", |
Assim, para executar nosso projeto, bastará escrever npm start na raiz do projeto e ele vai se achar corretamente.
Outra dica comum de organização é, assim como criamos uma pasta dist para os arquivos compilados em JS, crie uma pasta src para guardar os fontes TS do projeto, mantendo na raiz do projeto somente os arquivos de configuração e a pasta node_modules, como na imagem.
Para que o compilador do TS não se perca, isso exige que você edite outra configuração no tsconfig.json, como abaixo.
1 2 3 |
"rootDir": "./src/", |
A rootDir define o diretório raiz dos seus fontes em TS, necessária para o compilador se achar na hora que você mandar o comando tsc na raiz do projeto.
Recomendo agora que execute esta aplicação e realize os testes.
#5 – Autocompilação
Você deve imaginar que mesmo com o comando tsc na raiz do projeto compilando tudo, é bem chato ter de ficar chamando este comando entre uma alteração e outra antes de reexecutar sua aplicação, certo?
Uma estratégia bem simples que você pode adotar é a de alterar o seu script de start no packages.json para executar o tsc antes do node, como abaixo.
1 2 3 |
"start": "tsc ; nodemon ./dist/app", |
Assim, toda vez que você rodar um npm start, ele primeiro vai executar o tsc e depois a aplicação Node transpilada.
Mas isso ainda exige que você tenha de derrubar a sua aplicação e reexecutá-la a cada alteração. Como podemos melhorar isso?
Uma estratégia bem comum entre programadores Node.js é o uso de nodemon, um pacote muito popular que sobe a sua aplicação e fica observando alterações nos arquivos para reexecutá-la automaticamente. Eu já falei dele neste vídeo abaixo, que recomendo que assista caso nunca tenha usado nodemon antes.
O que nem todo mundo sabe é que você pode configurar um arquivo nodemon.json para que diga a ele observar arquivos .TS da sua aplicação e a rodá-la com um comando definido por você, que inclua a transpilação, como abaixo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
{ "restartable": "rs", "ignore": [ ".git", "dist", "node_modules/**/node_modules" ], "verbose": true, "events": { "restart": "osascript -e 'display notification \"App restarted due to:\n'$FILENAME'\" with title \"nodemon\"'" }, "env": { "NODE_ENV": "development" }, "ext": "ts,json" } |
Note que coloquei a pasta dist na configuração de ignore para que o nodemon não entre em loop. Outra alteração que fiz foi na configuração ext que lista as extensões que serão observadas pelo nodemon e que irão disparar a transpilação e reexecução.
Use esta web API como exemplo para avançar ainda mais nos seus estudos de TypeScript com Node.js. E se quiser mais alguns tutoriais de webapi em TypeScript, tem este aqui sem banco, este com Prisma ORM e este aqui de webapi com MongoDB.
Outro tópico com TS que recomendo que você estudo é sobre classes (POO).
Até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.