segurança no backend
JavaScript

Segurança em API REST com Node.js e Express: Boas Práticas no Backend

Criar uma API REST com Node.js e Express que funciona é apenas uma parte do desenvolvimento de uma aplicação.

O servidor pode iniciar corretamente, as rotas podem responder, o login pode funcionar e os dados podem chegar ao Backend sem nenhum problema.

Mas isso não significa que sua API esteja segura.

Uma informação sensível exposta no código, uma senha armazenada de maneira inadequada, a falta de validação ou até uma mensagem de erro detalhada demais podem criar problemas de segurança.

Foi exatamente esse o ponto de partida desta aula: começamos analisando uma API REST que funciona, mas possui problemas de segurança, e depois fomos corrigindo esses pontos utilizando Node.js e Express.

Neste artigo, você vai entender algumas boas práticas importantes de segurança no Backend, trabalhando conceitos como:

  • variáveis de ambiente;
  • proteção de senhas com bcrypt;
  • validação dos dados;
  • autenticação;
  • Async/Await;
  • Try/Catch;
  • códigos HTTP 400, 401 e 500;
  • logs;
  • JWT;
  • tratamento seguro de erros.

Importante: segurança de aplicações é um assunto muito maior do que conseguimos abordar em uma única aula. O objetivo aqui é construir uma base prática para quem está começando a desenvolver APIs.


Uma API funcionando pode continuar insegura

Imagine que você criou uma API REST.

Você inicia o servidor e tudo funciona.

O login responde.

As informações chegam corretamente.

Nenhum erro aparece.

Só que, ao analisar o código, encontramos situações como:

constSECRET='minha_chave_secreta';constusuario= {    email: 'usuario@email.com',    senha: '123456'};

Além disso, a aplicação pode estar respondendo:

Usuário não encontrado

ou:

Senha incorreta

E, quando acontece algum problema interno:

Erro ao acessar determinado arquivo...

A aplicação funciona, mas estamos expondo informações desnecessárias.

Foi justamente esse cenário que analisamos no início da aula: segredo exposto, senha sem o tratamento adequado, falta de validação e mensagens que poderiam fornecer informações demais para quem utiliza a API.

Essa é uma das primeiras ideias que precisamos entender:

Uma API funcionar não significa que ela esteja segura.


1. Não deixe informações sensíveis no código

Uma das primeiras melhorias relacionadas à segurança no Backend é separar informações sensíveis da lógica principal da aplicação.

Em vez de fazer:

constJWT_SECRET='minha_chave_secreta';

podemos utilizar variáveis de ambiente.

Para isso, podemos ter um arquivo .env:

PORT=3000JWT_SECRET=minha_chave_secreta

E acessar essas informações no Node.js:

require('dotenv').config();constPORT=process.env.PORT ||3000;constJWT_SECRET=process.env.JWT_SECRET;

Isso também ajuda a deixar o código principal do Backend mais organizado.

Cuidado com o .env no GitHub

Criar um arquivo .env e depois enviá-lo para um repositório público elimina boa parte da vantagem dessa separação.

Por isso, podemos adicioná-lo ao .gitignore:

node_modules/.env

Assim, o arquivo não deve ser versionado junto com o restante do projeto.

Em produção, essas informações normalmente são configuradas no próprio ambiente onde a aplicação será executada.

O fluxo fica mais ou menos assim:

DESENVOLVIMENTO      ↓Arquivo .env      ↓Não enviar informações sensíveis ao repositório      ↓PRODUÇÃO      ↓Variáveis configuradas no ambiente

2. Não armazene senhas em texto puro

Outro ponto importante de segurança está relacionado às senhas.

Evite armazenar algo assim:

constusuario= {    email: 'usuario@email.com',    senha: '123456'};

Nesse exemplo, temos a senha exatamente como ela foi informada.

Durante a aula utilizamos o bcrypt para trabalhar com o hash da senha.

constbcrypt=require('bcryptjs');constusuario= {    id: 1,    nome: 'Marcos',    email: 'marcos@email.com',    senhaHash: bcrypt.hashSync('123456', 10)};

No projeto da aula, utilizamos dados locais apenas para conseguirmos demonstrar o processo de autenticação sem precisar adicionar um banco de dados naquele momento.

Em um projeto real, naturalmente essa informação estaria associada à camada de persistência da aplicação.

O importante aqui é compreender o conceito:

Senha informada      ↓Hash      ↓Valor armazenado

Depois, durante o login, não precisamos recuperar a senha original. Fazemos a comparação utilizando o bcrypt.


3. Sempre valide os dados recebidos pela API REST

Uma API REST recebe informações externas.

Por isso, não devemos simplesmente confiar que tudo chegará exatamente como esperamos.

No nosso login precisamos de duas informações:

emailsenha

Podemos criar uma função específica para validar esses dados:

functionvalidarLogin(email, senha) {if (!email||!senha) {return'E-mail e senha são obrigatórios';    }if (typeofemail!=='string'||typeofsenha!=='string'    ) {return'E-mail e senha devem ser textos';    }returnnull;}

Essa função possui uma responsabilidade bem definida: validar os dados do login.

Se existir algum problema, retornamos uma mensagem.

Caso contrário:

returnnull;

Na implementação da aula, essa validação acontece antes de continuarmos com a busca do usuário e a verificação das credenciais.

Além da segurança, separar a validação em uma função também ajuda na organização e evita duplicação de código.


4. Criando a rota de login no Backend

Para receber os dados do login, criamos uma rota POST:

app.post('/login', async (req, res) => {});

O cliente pode enviar um JSON semelhante a:

{    "email": "marcos@email.com",    "senha": "123456"}

No Backend, esses dados chegam pelo corpo da requisição.

Podemos acessá-los utilizando:

req.body

E podemos utilizar a desestruturação:

const { email, senha } =req.body;

É como se fizéssemos:

constemail=req.body.email;constsenha=req.body.senha;

Durante a aula, utilizamos justamente esse exemplo para mostrar o “desempacotamento” das informações que chegaram no JSON.

O fluxo é:

FRONT-END    ↓JSON    ↓REQUISIÇÃO    ↓API REST    ↓BACKEND    ↓req.body

Entender esse caminho facilita bastante a compreensão da integração entre Front-end e Backend.


5. Validando a requisição com o Status HTTP 400

Agora podemos utilizar a função criada anteriormente:

consterroValidacao=validarLogin(email, senha);

Se existir algum erro:

if (erroValidacao) {returnres.status(400).json({        erro: erroValidacao    });}

Nesse caso utilizamos o Status HTTP 400.

Somente depois que os dados passam por essa primeira validação continuamos o processamento do login.

Isso é muito melhor do que simplesmente receber qualquer informação e começar imediatamente a utilizá-la dentro da aplicação.


6. Buscando o usuário

Depois de validar os dados, precisamos descobrir se existe um usuário correspondente.

Como estamos utilizando um vetor para simplificar o exemplo, podemos fazer:

constusuario=usuarios.find(item => item.email ===email);

O find() procura o primeiro elemento que atende à condição.

Na aula, utilizamos exatamente essa lógica para comparar o e-mail recebido pelo Backend com os usuários disponíveis no nosso exemplo.

Se estivéssemos utilizando um banco de dados, essa etapa seria substituída pela consulta correspondente.

O raciocínio continuaria semelhante:

Recebe o e-mail      ↓Valida      ↓Busca o usuário      ↓Encontrou?      ↓Continua a autenticação

7. Por que não retornar “usuário não encontrado”?

Aqui temos um detalhe pequeno no código, mas muito interessante quando falamos de segurança em API REST.

Imagine que alguém tenta fazer login e recebe:

Usuário não encontrado

Agora tenta outro e recebe:

Senha incorreta

Existe uma diferença importante.

No segundo caso, a própria aplicação acabou de informar que aquele usuário provavelmente existe.

Quem estiver realizando tentativas indevidas já possui parte da informação.

Por isso podemos utilizar uma mensagem genérica:

if (!usuario) {returnres.status(401).json({        erro: 'Usuário ou senha inválidos'    });}

A mesma mensagem pode aparecer quando a senha estiver errada.

Na aula, esse cuidado foi explicado justamente para evitar fornecer “50% da informação” para alguém que esteja tentando descobrir credenciais válidas.


8. Erro 401 na autenticação

O Status HTTP 401 está relacionado à autenticação.

No nosso exemplo:

returnres.status(401).json({    erro: 'Usuário ou senha inválidos'});

Perceba que não precisamos informar:

Usuário está correto, mas a senha está errada.

Nem:

Senha correta, usuário inexistente.

Simplesmente:

Usuário ou senha inválidos.

Isso reduz a quantidade de informação fornecida por nossa API REST durante uma tentativa de autenticação.


9. Async/Await no Backend

Durante a criação da rota aparece outra dúvida muito comum de quem está começando com Node.js:

asyncawait

O conceito fica mais fácil quando pensamos que algumas operações precisam aguardar um resultado antes que o algoritmo continue.

Isso acontece bastante quando trabalhamos com:

API externaBanco de dadosArquivosAutenticaçãoOutras operações assíncronas

No nosso exemplo:

constsenhaCorreta=awaitbcrypt.compare(senha,usuario.senhaHash);

Precisamos saber o resultado da comparação antes de decidir se o login pode continuar.

Por isso utilizamos await.

Na aula, esse conceito é apresentado justamente dentro do contexto de operações que dependem de respostas externas ou assíncronas no Backend.


10. Comparando a senha com bcrypt

Depois de localizar o usuário, fazemos a comparação:

constsenhaCorreta=awaitbcrypt.compare(senha,usuario.senhaHash);

Depois verificamos:

if (!senhaCorreta) {returnres.status(401).json({        erro: 'Usuário ou senha inválidos'    });}

Observe novamente que utilizamos a mesma resposta.

Não importa se foi o usuário ou a senha que causou a falha na autenticação.

Para quem está utilizando a API:

Usuário ou senha inválidos

Na construção realizada durante a live, await, bcrypt.compare() e o Status 401 aparecem justamente nessa etapa.


11. Entendendo Try/Catch

Outra estrutura extremamente importante no desenvolvimento Backend é:

try {} catch (erro) {}

Dentro do try, executamos o código principal.

Se alguma operação gerar uma exceção, podemos capturar o problema no catch.

Exemplo:

app.post('/login', async (req, res) => {try {// lógica do login    } catch (erro) {console.error('Erro interno no login:', erro);returnres.status(500).json({            erro: 'Erro interno do servidor'        });    }});

Essa estrutura se torna ainda mais importante quando trabalhamos com APIs, banco de dados e serviços externos.

Na aula, o try/catch também foi utilizado para mostrar que precisamos conseguir identificar internamente o que aconteceu quando alguma operação falha.


12. O usuário não precisa ver o mesmo erro que o desenvolvedor

Aqui temos uma distinção fundamental para a segurança do Backend.

Quando algo falha, existem pelo menos duas necessidades diferentes.

Desenvolvedor

Precisa de informações suficientes para investigar o problema.

console.error('Erro interno no login:', erro);

Usuário

Precisa saber que alguma coisa deu errado, mas não necessariamente conhecer detalhes internos da aplicação.

returnres.status(500).json({    erro: 'Erro interno do servidor'});

São públicos diferentes.

E, portanto, as informações também podem ser diferentes.


13. Erro 500: cuidado com o que sua API revela

Um dos pontos que mais destacamos na aula foi o erro 500.

Imagine fazer:

catch (erro) {returnres.status(500).json({        erro: erro    });}

Dependendo do erro e da estrutura da aplicação, podemos acabar expondo informações internas desnecessárias.

Uma abordagem melhor para nosso exemplo é:

catch (erro) {console.error('Erro interno no login:', erro);returnres.status(500).json({        erro: 'Erro interno do servidor'    });}

Assim:

DESENVOLVEDOR      ↓Informações detalhadas no logUSUÁRIO      ↓Erro interno do servidor

Durante a live, esse foi apresentado como um dos problemas mais graves da API inicial: retornar detalhes específicos de um erro interno diretamente para quem fez a requisição.

E, durante a implementação, reforçamos novamente que o erro detalhado deve ser útil internamente, enquanto o cliente recebe uma resposta genérica.


14. Logs ajudam a descobrir problemas no Backend

Esconder informações internas do usuário não significa esconder os erros do desenvolvedor.

Precisamos conseguir descobrir o que está acontecendo.

Podemos utilizar:

console.log('Servidor iniciado');

Para erros:

console.error('Erro interno no login:', erro);

E também avisos:

console.warn('JWT_SECRET não configurado');

Na aula também comentamos a diferença semântica entre log, error e warn.

Em aplicações maiores, naturalmente podemos utilizar ferramentas próprias para logs e monitoramento.

Mas para quem está começando, é importante compreender desde cedo a ideia:

O usuário precisa de uma resposta segura. O desenvolvedor precisa de informações para diagnosticar o problema.


15. JWT e informações secretas

Durante a aula também deixamos preparada a ideia do JWT_SECRET.

O JWT pode fazer parte do processo de autenticação de uma aplicação, mas nesta aula não implementamos todo o processo de criação do token, payload e demais configurações.

O objetivo foi mostrar onde esse tipo de informação entraria e, principalmente, reforçar que uma chave secreta não deveria ficar exposta diretamente no código.

Podemos inclusive verificar sua existência:

if (!process.env.JWT_SECRET) {console.warn('JWT_SECRET não configurado');}

A implementação completa de JWT fica para uma etapa específica, porque envolve outros conceitos que merecem ser estudados com mais calma. Isso também foi deixado claro durante a própria aula.


16. Front-end e Backend precisam falar a mesma língua

Observe uma resposta da nossa API:

returnres.status(401).json({    erro: 'Usuário ou senha inválidos'});

Temos uma propriedade chamada:

erro

O Front-end precisa saber que essa propriedade existe para conseguir apresentar sua mensagem.

A resposta da API REST será semelhante a:

{    "erro": "Usuário ou senha inválidos"}

Isso mostra como entender Backend também ajuda no desenvolvimento Front-end.

A interface não recebe uma informação mágica.

Existe uma estrutura definida na API que precisa ser interpretada corretamente.

Esse relacionamento entre a propriedade criada no JSON e aquilo que posteriormente será utilizado pelo Front-end também foi demonstrado durante a aula.


17. Somente depois das validações retornamos sucesso

Depois de validar os dados, localizar o usuário e comparar a senha, podemos finalmente retornar uma resposta de sucesso:

returnres.status(200).json({    mensagem: 'Login realizado com sucesso',    usuario: {        id: usuario.id,        nome: usuario.nome,        email: usuario.email    }});

Observe quantas coisas aconteceram antes:

Receber os dados       ↓Validar       ↓Buscar usuário       ↓Verificar credenciais       ↓Comparar senha       ↓Tratar possíveis erros       ↓Retornar sucesso

Na aplicação construída na aula, o 200 só é retornado depois que essas verificações são realizadas.

Isso demonstra uma ideia importante:

Segurança não é uma única função adicionada à API. São várias decisões tomadas durante o desenvolvimento.


18. Teste sua API durante o desenvolvimento

Não espere terminar todo o Backend para descobrir se ele funciona.

Durante a aula fomos testando as etapas da aplicação.

Um fluxo simples ajuda bastante:

IMPLEMENTA    ↓TESTA    ↓FUNCIONOU?    ↓CONTINUA

Ao testar o login, por exemplo, conseguimos verificar uma tentativa inválida e posteriormente uma autenticação bem-sucedida. A própria captura dos erros também ajuda o desenvolvedor a localizar rapidamente onde está o problema.

Para quem está começando com Node.js, Express e API REST, esse hábito evita passar muito tempo procurando um erro que foi introduzido várias etapas antes.


Segurança em API REST vai muito além deste projeto

Tudo que vimos aqui representa apenas uma introdução.

Quando começamos a desenvolver aplicações maiores, aparecem outros assuntos importantes de segurança, como:

  • JWT;
  • sessões;
  • cookies;
  • middlewares;
  • autorização;
  • proteção de rotas;
  • limitação de tentativas de login;
  • validações mais completas;
  • controle de permissões;
  • proteção das informações;
  • monitoramento e logs;
  • outras formas de ataque e proteção.

A própria live deixa claro desde o começo que JWT, sessões, cookies, middlewares e outras camadas de segurança precisam ser estudadas separadamente.

Não tente aprender tudo de uma vez.

Primeiro entenda o fluxo.

Depois vá adicionando novas camadas de segurança ao Backend.


📺 Assista à aula completa

Este artigo foi desenvolvido a partir da Live 128 do Desvendando o Código, onde construímos e testamos o projeto passo a passo.

Segurança em API REST com Node.js e Express | Boas Práticas na Prática

No vídeo você pode acompanhar todo o desenvolvimento da API REST, a construção da rota de login e os testes realizados no Backend com Node.js e Express.


Conclusão

Criar uma API REST não significa apenas fazer as rotas funcionarem.

Quando trabalhamos com Backend, também precisamos pensar nas informações que recebemos, processamos, armazenamos e devolvemos.

Ao longo deste projeto vimos cuidados importantes de segurança, como proteger informações sensíveis, não armazenar senhas em texto puro, utilizar bcrypt, validar os dados recebidos, tratar erros com try/catch, utilizar async/await, trabalhar corretamente com os códigos HTTP e evitar retornar informações internas da aplicação.

Principalmente:

Uma API funcionando não significa uma API segura.

A segurança no Backend precisa fazer parte das decisões tomadas durante o desenvolvimento da aplicação.

Comece pelos fundamentos apresentados aqui e vá evoluindo sua API REST com Node.js e Express conforme aprender novas técnicas de autenticação, autorização e proteção de dados.

camadas de segurança podem ser adicionadas à aplicação.

Leave a Reply

Your email address will not be published. Required fields are marked *