Ao construir um back-end para uma API REST, Express.js é frequentemente a primeira escolha entre os frameworks Node.js. Embora também suporte a construção de HTML estático e modelos, nesta série, vamos nos concentrar no desenvolvimento de back-end usando TypeScript. A API REST resultante será aquela que qualquer estrutura de front-end ou serviço de back-end externo seria capaz de consultar.
Você precisará:
Em um terminal (ou prompt de comando), criaremos uma pasta para o projeto. Nessa pasta, execute npm init
. Isso vai criar alguns dos arquivos básicos de projeto Node.js de que precisamos.
A seguir, adicionaremos a estrutura Express.js e algumas bibliotecas úteis:
npm install --save express debug winston express-winston cors
Existem boas razões para essas bibliotecas serem Desenvolvedor Node.js favoritos:
debug
é um módulo que usaremos para evitar a chamada de console.log()
ao desenvolver nosso aplicativo. Dessa forma, podemos filtrar facilmente as instruções de depuração durante a solução de problemas. Eles também podem ser totalmente desligados na produção, em vez de serem removidos manualmente.winston
é responsável por registrar solicitações à nossa API e pelas respostas (e erros) retornadas. express-winston
integra-se diretamente com Express.js, de modo que todos os padrões relacionados à API winston
código de registro já está feito.cors
é um middleware Express.js que nos permite habilitar compartilhamento de recursos de origem cruzada . Sem isso, nossa API só poderia ser usada em front-ends servidos exatamente no mesmo subdomínio de nosso back-end.Nosso back end usa esses pacotes quando está em execução. Mas também precisamos instalar alguns desenvolvimento dependências para nossa configuração TypeScript. Para isso, vamos executar:
npm install --save-dev @types/cors @types/express @types/debug source-map-support tslint typescript
Essas dependências são necessárias para habilitar o TypeScript para o código do nosso aplicativo, junto com os tipos usados por Express.js e outras dependências. Isso pode economizar muito tempo quando estamos usando um IDE como WebStorm ou VSCode, permitindo-nos completar alguns métodos de função automaticamente durante a codificação.
As dependências finais em package.json
deve ser assim:
o que pode causar um vazamento de memória
'dependencies': { 'debug': '^4.2.0', 'express': '^4.17.1', 'express-winston': '^4.0.5', 'winston': '^3.3.3', 'cors': '^2.8.5' }, 'devDependencies': { '@types/cors': '^2.8.7', '@types/debug': '^4.1.5', '@types/express': '^4.17.2', 'source-map-support': '^0.5.16', 'tslint': '^6.0.0', 'typescript': '^3.7.5' }
Agora que temos todas as dependências necessárias instaladas, vamos começar a construir nosso próprio código!
Para este tutorial, vamos criar apenas três arquivos:
./app.ts
./common/common.routes.config.ts
./users/users.routes.config.ts
A ideia por trás das duas pastas da estrutura do projeto (common
e users
) é ter módulos individuais com suas próprias responsabilidades. Nesse sentido, eventualmente teremos alguns ou todos os itens a seguir para cada módulo:
Essa estrutura de pastas fornece um ponto de partida precoce para o restante desta série de tutoriais e o suficiente para começar a praticar.
No common
, vamos criar a pasta common.routes.config.ts
arquivo parecido com o seguinte:
import express from 'express'; export class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; } getName() { return this.name; } }
A forma como estamos criando as rotas aqui é opcional. Mas, como estamos trabalhando com TypeScript, nosso cenário de rotas é uma oportunidade para praticar o uso de herança com o extends
palavra-chave, como veremos em breve. Neste projeto, todos os arquivos de rota têm o mesmo comportamento: Eles têm um nome (que usaremos para fins de depuração) e acesso ao Express.js principal Application
objeto.
Agora, podemos começar a criar o arquivo de rota de usuários. No users
pasta, vamos criar users.routes.config.ts
e comece a codificá-lo assim:
import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } }
Aqui, estamos importando o CommonRoutesConfig
classe e estendendo-a para nossa nova classe, chamada UsersRoutes
. Com o construtor, enviamos o aplicativo (o objeto principal express.Application
) e o nome UsersRoutes para o construtor CommonRoutesConfig
.
Este exemplo é bastante simples, mas ao dimensionar para criar vários arquivos de rota, isso nos ajudará a evitar código duplicado.
Suponha que queiramos adicionar novos recursos a este arquivo, como o registro. Poderíamos adicionar o campo necessário ao CommonRoutesConfig
classe e, em seguida, todas as rotas que estendem CommonRoutesConfig
terá acesso a ele.
E se quisermos ter alguma funcionalidade que seja semelhante entre essas classes (como configurar os terminais da API), mas isso precisa de uma implementação diferente para cada classe? Uma opção é usar um recurso TypeScript chamado abstração .
Vamos criar uma função abstrata muito simples que UsersRoutes
classe (e futuras classes de roteamento) herdarão de CommonRoutesConfig
. Digamos que queremos forçar todas as rotas a ter uma função (para que possamos chamá-la de nosso construtor comum) chamada configureRoutes()
. É onde declararemos os endpoints de cada recurso da classe de roteamento.
Para fazer isso, vamos adicionar três coisas rápidas a common.routes.config.ts
:
abstract
para o nosso class
linha, para permitir a abstração para esta classe.abstract configureRoutes(): express.Application;
. Isso força qualquer classe estendendo CommonRoutesConfig
para fornecer uma implementação que corresponda a essa assinatura - se não, o compilador do TypeScript gerará um erro.this.configureRoutes();
no final do construtor, pois agora podemos ter certeza de que esta função existirá.O resultado:
import express from 'express'; export abstract class CommonRoutesConfig { app: express.Application; name: string; constructor(app: express.Application, name: string) { this.app = app; this.name = name; this.configureRoutes(); } getName() { return this.name; } abstract configureRoutes(): express.Application; }
Com isso, qualquer classe que estenda CommonRoutesConfig
deve ter uma função chamada configureRoutes()
que retorna um express.Application
objeto. Isso significa users.routes.config.ts
precisa de atualização:
import {CommonRoutesConfig} from '../common/common.routes.config'; import express from 'express'; export class UsersRoutes extends CommonRoutesConfig { constructor(app: express.Application) { super(app, 'UsersRoutes'); } configureRoutes() { // (we'll add the actual route configuration here next) return this.app; } }
Para recapitular o que fizemos:
Estamos primeiro importando o common.routes.config
arquivo, então o express
módulo. Em seguida, definimos o UserRoutes
classe, dizendo que queremos estender o CommonRoutesConfig
classe base, o que implica que prometemos que ela implementará configureRoutes()
.
Para enviar informações junto com CommonRoutesConfig
, estamos usando a classe constructor
da classe. Ele espera receber o express.Application
objeto, que descreveremos em maior profundidade na próxima etapa. Com super()
, passamos para o construtor de CommonRoutesConfig
o aplicativo e o nome de nossas rotas, que neste cenário é UsersRoutes. (super()
, por sua vez, chamará nossa implementação de configureRoutes()
.)
O configureRoutes()
é onde criaremos os terminais para usuários de nossa API REST. Lá, vamos usar o inscrição e os seus rota funcionalidades do Express.js.
A ideia de usar o app.route()
função é evitar a duplicação de código, o que é fácil, pois estamos criando uma API REST com recursos bem definidos. O principal recurso para este tutorial é Comercial . Temos dois casos neste cenário:
users
no final do caminho solicitado. (Não entraremos em filtragem de consulta, paginação ou outras consultas semelhantes neste artigo.)users/:userId
.O caminho .route()
funciona em Express.js nos permite lidar com verbos HTTP com algum encadeamento elegante. Isso ocorre porque .get()
, .post()
, etc., todos retornam a mesma instância de IRoute
que o primeiro .route()
ligar faz. A configuração final será assim:
configureRoutes() { this.app.route(`/users`) .get((req: express.Request, res: express.Response) => { res.status(200).send(`List of users`); }) .post((req: express.Request, res: express.Response) => { res.status(200).send(`Post to users`); }); this.app.route(`/users/:userId`) .all((req: express.Request, res: express.Response, next: express.NextFunction) => { // this middleware function runs before any request to /users/:userId // but it doesn't accomplish anything just yet--- // it simply passes control to the next applicable function below using next() next(); }) .get((req: express.Request, res: express.Response) => { res.status(200).send(`GET requested for id ${req.params.userId}`); }) .put((req: express.Request, res: express.Response) => { res.status(200).send(`PUT requested for id ${req.params.userId}`); }) .patch((req: express.Request, res: express.Response) => { res.status(200).send(`PATCH requested for id ${req.params.userId}`); }) .delete((req: express.Request, res: express.Response) => { res.status(200).send(`DELETE requested for id ${req.params.userId}`); }); return this.app; }
O código acima permite que qualquer cliente REST API chame nosso users
ponto final com um POST
ou um GET
solicitação. Da mesma forma, permite que um cliente ligue para o nosso /users/:userId
ponto final com um GET
, PUT
, PATCH
ou DELETE
solicitação.
Mas para /users/:userId
, também adicionamos middleware genérico usando o all()
, que será executada antes de get()
, put()
, patch()
ou delete()
funções. Esta função será benéfica quando (posteriormente na série) criarmos rotas que devem ser acessadas apenas por usuários autenticados.
Você deve ter notado que em nosso .all()
função — como acontece com qualquer parte de middleware — temos três tipos de campos: Request
, Response
e NextFunction
.
NextFunction
serve como uma função de retorno de chamada, permitindo que o controle passe por quaisquer outras funções de middleware. Ao longo do caminho, todo middleware compartilhará os mesmos objetos de solicitação e resposta antes que o controlador finalmente envie uma resposta de volta ao solicitante.app.ts
Agora que configuramos alguns esqueletos de rota básicos, vamos começar a configurar o ponto de entrada do aplicativo. Vamos criar o app.ts
arquivo na raiz da pasta do nosso projeto e comece com este código:
import express from 'express'; import * as http from 'http'; import * as bodyparser from 'body-parser'; import * as winston from 'winston'; import * as expressWinston from 'express-winston'; import cors from 'cors'; import {CommonRoutesConfig} from './common/common.routes.config'; import {UsersRoutes} from './users/users.routes.config'; import debug from 'debug';
Apenas duas dessas importações são novas neste ponto do artigo:
http
é um módulo nativo do Node.js. É necessário iniciar nosso aplicativo Express.js.body-parser
é um middleware que vem com Express.js. Ele analisa a solicitação (em nosso caso, como JSON) antes que o controle vá para nossos próprios manipuladores de solicitação.Agora que importamos os arquivos, vamos começar a declarar as variáveis que queremos usar:
diferença entre llc s corp e c corp
const app: express.Application = express(); const server: http.Server = http.createServer(app); const port: Number = 3000; const routes: Array = []; const debugLog: debug.IDebugger = debug('app');
O express()
função retorna o objeto de aplicativo Express.js principal que passaremos por todo o nosso código, começando por adicioná-lo ao http.Server
objeto. (Precisamos iniciar o http.Server
após configurar nosso express.Application
.)
Ouviremos na porta 3000 em vez das portas padrão 80 (HTTP) ou 443 (HTTPS) porque elas normalmente seriam usadas para o front-end de um aplicativo.
Não há regra segundo a qual a porta deve ser 3000 - se não for especificada, uma porta arbitrária será atribuído, mas 3000 é usado em todos os exemplos de documentação para Node.js e Express.js, portanto, continuamos a tradição aqui.
Ainda podemos executar localmente em uma porta personalizada, mesmo quando queremos que nosso back-end responda às solicitações em portas padrão. Isso exigiria um proxy reverso para receber solicitações na porta 80 ou 443 com um domínio ou subdomínio específico. Em seguida, ele os redirecionaria para nossa porta interna 3000.
O routes
array controlará nossos arquivos de rotas para fins de depuração, como veremos a seguir.
Finalmente, debugLog
acabará como uma função semelhante a console.log
, mas melhor: é mais fácil de ajustar porque é automaticamente definido para tudo o que quisermos chamar nosso contexto de arquivo / módulo. (Neste caso, o chamamos de 'app' quando passamos isso em uma string para o debug()
construtor.)
Agora, estamos prontos para configurar todos os nossos módulos de middleware Express.js e as rotas de nossa API:
// here we are adding middleware to parse all incoming requests as JSON app.use(bodyparser.json()); // here we are adding middleware to allow cross-origin requests app.use(cors()); // here we are configuring the expressWinston logging middleware, // which will automatically log all HTTP requests handled by Express.js app.use(expressWinston.logger({ transports: [ new winston.transports.Console() ], format: winston.format.combine( winston.format.colorize(), winston.format.json() ) })); // here we are adding the UserRoutes to our array, // after sending the Express.js application object to have the routes added to our app! routes.push(new UsersRoutes(app)); // here we are configuring the expressWinston error-logging middleware, // which doesn't *handle* errors per se, but does *log* them app.use(expressWinston.errorLogger({ transports: [ new winston.transports.Console() ], format: winston.format.combine( winston.format.colorize(), winston.format.json() ) })); // this is a simple route to make sure everything is working properly app.get('/', (req: express.Request, res: express.Response) => { res.status(200).send(`Server up and running!`) });
Você deve ter notado que expressWinston.errorLogger
está definido depois de nós definimos nossas rotas. Isso não é um erro! Enquanto o documentação express-winston afirma:
O logger precisa ser adicionado APÓS o roteador expresso (
app.router
) e ANTES de qualquer um de seus manipuladores de erro personalizados (express.handler
).
Finalmente e mais importante:
server.listen(port, () => { debugLog(`Server running at http://localhost:${port}`); routes.forEach((route: CommonRoutesConfig) => { debugLog(`Routes configured for ${route.getName()}`); }); });
Isso realmente inicia nosso servidor. Assim que for iniciado, o Node.js executará nossa função de retorno de chamada, que relata que estamos executando, seguida pelos nomes de todas as rotas que configuramos - até agora, apenas UsersRoutes
.
package.json
para Transpilar TypeScript para JavaScript e executar o aplicativoAgora que temos nosso esqueleto pronto para ser executado, primeiro precisamos de alguma configuração padrão para habilitar a transpilação do TypeScript. Vamos adicionar o arquivo tsconfig.json
na raiz do projeto:
{ 'compilerOptions': { 'target': 'es2016', 'module': 'commonjs', 'outDir': './dist', 'strict': true, 'esModuleInterop': true, 'inlineSourceMap': true } }
Então, só precisamos adicionar os toques finais a package.json
na forma dos seguintes scripts:
'scripts': { 'start': 'tsc && node ./dist/app.js', 'debug': 'export DEBUG=* && npm run start', 'test': 'echo 'Error: no test specified' && exit 1' },
O test
script é um marcador que substituiremos posteriormente na série.
o tsc no start
o script pertence ao TypeScript. É responsável por transpilar nosso código TypeScript em JavaScript, que será gerado em dist
pasta. Então, apenas rodamos a versão construída com node ./dist/app.js
.
O debug
script chama o start
script, mas primeiro define um DEBUG
variável de ambiente. Isso tem o efeito de habilitar todos os nossos debugLog()
instruções (mais outras semelhantes do próprio Express.js, que usa o mesmo debug
módulo que usamos) para enviar detalhes úteis para o terminal - detalhes que estão (convenientemente) ocultos ao executar o servidor no modo de produção com um npm start
.
Tente executar npm run debug
você mesmo, e depois, compare com npm start
para ver como a saída do console muda.
Dica: Você pode limitar a saída de depuração para nosso app.ts
do próprio arquivo debugLog()
declarações usando DEBUG=app
em vez de DEBUG=*
. O debug
módulo geralmente é bastante flexível, e este recurso não é exceção .
Os usuários do Windows provavelmente precisarão alterar o export
para SET
desde export
é assim que funciona no Mac e no Linux. Se o seu projeto precisa oferecer suporte a vários ambientes de desenvolvimento, o pacote cross-env fornece uma solução simples aqui.
Com npm run debug
ou npm start
ainda em andamento, nossa API REST estará pronta para solicitações de serviço na porta 3000. Neste ponto, podemos usar cURL, Carteiro , Insônia , etc. para testar o backend.
Como criamos apenas um esqueleto para o recurso de usuários, podemos simplesmente enviar solicitações sem corpo para ver se tudo está funcionando conforme o esperado. Por exemplo:
curl --location --request GET 'localhost:3000/users/12345'
Nosso backend deve enviar de volta a resposta GET requested for id 12345
.
Quanto a POST
ing:
curl --location --request POST 'localhost:3000/users' --data-raw ''
Este e todos os outros tipos de solicitações para as quais construímos esqueletos serão bastante semelhantes.
Neste artigo, começamos a criar uma API REST configurando o projeto do zero e mergulhando nos fundamentos da estrutura Express.js. Em seguida, demos nosso primeiro passo para dominar o TypeScript, criando um padrão com UsersRoutesConfig
estendendo CommonRoutesConfig
, um padrão que reutilizaremos no próximo artigo desta série. Terminamos configurando nosso app.ts
ponto de entrada para usar nossas novas rotas e package.json
com scripts para construir e executar nosso aplicativo.
Mas mesmo os fundamentos de uma API REST feita com Express.js e TypeScript são bastante envolventes. Dentro a próxima parte desta série, nos concentramos na criação de controladores adequados para os recursos dos usuários e nos aprofundamos em alguns padrões úteis para serviços, middleware, controladores e modelos.
O projeto completo está disponível no GitHub , e o código no final deste artigo é encontrado no toptal-article-01
ramo.
Absolutamente! É muito comum que pacotes npm populares (incluindo Express.js) tenham arquivos de definição de tipo TypeScript correspondentes. Isso é verdade sobre o próprio Node.js, além de subcomponentes incluídos como seu pacote de depuração.
Sim. O Node.js pode ser usado sozinho para criar APIs REST prontas para produção, e também existem várias estruturas populares como Express.js para reduzir o boilerplate inevitável.
Não, não é difícil começar a aprender TypeScript para quem tem um background moderno de JavaScript. É ainda mais fácil para quem tem experiência em programação orientada a objetos. Mas dominar todas as nuances e práticas recomendadas do TypeScript leva tempo, como acontece com qualquer habilidade.
Depende do projeto, mas é definitivamente recomendado para programação em Node.js. É uma linguagem mais expressiva para modelar domínios de problemas do mundo real no back end. Isso torna o código mais legível e reduz o potencial de bugs.
O TypeScript é usado em qualquer lugar que o JavaScript seja encontrado, mas é especialmente adequado para aplicativos maiores. Ele usa JavaScript como base, adicionando tipagem estática e um suporte muito melhor para o paradigma de programação orientada a objetos (OOP). Isso, por sua vez, oferece suporte a uma experiência mais avançada de desenvolvimento e depuração.