DEV Community

Matheus de Gondra
Matheus de Gondra

Posted on

Preparando uma API NodeJS para produção

Hoje vou mostrar o que deve ser feito para preparar sua aplicação NodeJS para o ambiente de produção. Mas, antes vamos criar um projeto para isso.

Iniciando projeto

A versão do NodeJS que estarei usando é v22.17.0 e usarei o pnpm na versão v10.18.3 como o gerenciador de pacotes, mas você pode usar o npm ou o yarn se preferir.

Vamos iniciar um projeto com o comando init para gerar o package.json

pnpm init
Enter fullscreen mode Exit fullscreen mode

Vou adicionar a chave "type": "module" para usarmos o ES Modules nos nossos arquivos.

{
  "name": "node-prod",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module", // Usando ES Modules
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.18.3"
}
Enter fullscreen mode Exit fullscreen mode

Instalando dependências

Vamos criar API REST, então precisamos instalar algumas dependências no projeto. Vamos ter dependências de produção, que ficam em dependencies e dependências de desenvolvimento, que ficam em devDependencies, tudo isso listado no package.json.

Vamos começar instalando as dependências de produção

  • express: criação do servidor HTTP
  • pino: geração de logs
  • @prisma/client: cliente do banco de dados
  • @prisma/adapter-pg: adaptador do postgres para o prisma client
  • pg: driver do banco PostgreSQL
  • dotenv: para acessar as variáveis de ambiente no .env
  • express-rate-limit: middleware contra ataque de força bruta
  • cors: middleware para configurar o CORS
  • helmet: middleware para adicionar cabeçalhos de segurança
pnpm add express pino @prisma/client @prisma/adapter-pg pg dotenv express-rate-limit cors helmet
Enter fullscreen mode Exit fullscreen mode

Essas são as dependências que serão usadas no ambiente de produção. Agora vamos instalar as dependências que serão usadas apenas no ambiente de desenvolvimento.

  • typescript: superset do javascript que adiciona tipagem estática
  • prisma: biblioteca de ORM
  • tsx: para executar código typescript
  • @tsconfig/node22: configuração do typescript para o NodeJS v22
  • pino-pretty: para melhor visualização dos logs em desenvolvimento
  • @types/pg: tipagem do pg
  • @types/node: tipagem do NodeJS
  • @types/express: tipagem do express
  • @types/cors: tipagem do cors
pnpm add -D typescript prisma tsx @tsconfig/node22 @types/pg @types/node @types/express @types/cors pino-pretty
Enter fullscreen mode Exit fullscreen mode

No pnpm há uma particularidade, algumas biblioteca têm um script postinstall que o pnpm bloqueia por segurança. Para desbloquear isso você precisa usar o comando pnpm approve-builds e escolher quais libs você permite rodar o script postinstall e digita y para rodar os scripts de postinstall.

Com isso, um arquivo pnpm-workspace.yaml também será criado listando as bibliotecas que podem executar o postinstall.

onlyBuiltDependencies:
  - '@prisma/engines'
  - esbuild
  - prisma
Enter fullscreen mode Exit fullscreen mode

O package.json ficou assim.

{
  "name": "node-prod",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "packageManager": "pnpm@10.18.3",
  "dependencies": {
    "@prisma/adapter-pg": "^7.2.0",
    "@prisma/client": "^7.2.0",
    "cors": "^2.8.5",
    "dotenv": "^17.2.3",
    "express": "^5.2.1",
    "express-rate-limit": "^8.2.1",
    "helmet": "^8.1.0",
    "pg": "^8.16.3",
    "pino": "^10.1.0"
  },
  "devDependencies": {
    "@tsconfig/node22": "^22.0.5",
    "@types/cors": "^2.8.19",
    "@types/express": "^5.0.6",
    "@types/node": "^25.0.3",
    "@types/pg": "^8.16.0",
    "pino-pretty": "^13.1.3",
    "prisma": "^7.2.0",
    "tsx": "^4.21.0",
    "typescript": "^5.9.3"
  }
}
Enter fullscreen mode Exit fullscreen mode

Configurando typescript

Agora vamos configurar o typescript. Use o comando tsc --init para criar o arquivo tsconfig.json. E vamos usar a configuração que instalamos com o @tsconfig/node22, vamos definir o diretório de que irá ser gerado com o código compilado como dist e não vamos permitir a compilação se houver erros. Vamos usar o include para incluir tudo de src na pasta dist quando for gerada, se não seria criada uma pasta src dentro da pasta dist.

pnpm tsc --init
Enter fullscreen mode Exit fullscreen mode

tsconfig.json

{
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "noEmitOnError": true
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

Variáveis de ambiente

Agora vamos criar nossos arquivos typescript. Vamos começar com um arquivo src/env.ts para centralizar as variáveis de ambiente que usaremos.

export const env = {
  port: Number(process.env.PORT) || 3000,
  isProduction: process.env.NODE_ENV === "production",
  databaseUrl: process.env.DATABASE_URL,
  logLevel: process.env.LOG_LEVEL || "info"
};
Enter fullscreen mode Exit fullscreen mode

Logger da aplicação

Agora vamos criar o logger para a nossa aplicação. Usaremos o pino que instalamos antes para isso. Vamos criar o arquivo src/logger.ts.

Na configuração do logger iremos definir o nível de log, para que mensagem de log que estejam abaixo daquele nível não apareçam. E vamos usar o pino-pretty quando não estivermos em ambiente de produção.

import pino from "pino";
import { env } from "./env.js";

export const logger = pino({
  level: env.logLevel,
  transport: env.isProduction ? undefined : { target: "pino-pretty" }
});
Enter fullscreen mode Exit fullscreen mode

Banco de dados

Para a comunicação com o banco de dados vamos usar o Prisma como biblioteca de ORM e o PostgreSQL como banco de dados.

Primeiro precisamos configurar o prisma com o comando init do prisma.

pnpm prisma init
Enter fullscreen mode Exit fullscreen mode

Esse comando irá criar o prisma/schema.prisma onde temos os esquemas, .env onde ficará as variáveis de ambiente, .gitignore para impedir que alguns arquivos sejam salvos pelo git e o prisma.config.ts onde terá a configuração da CLI do prisma.

No prisma/schema.prisma vamos criar a tabela de usuários. Usarei o @map para mudar o nome da coluna no banco e @@map para mudar o nome da tabela no banco.

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id        Int      @id @default(autoincrement())
  name      String
  email     String   @unique
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  @@map("users")
}
Enter fullscreen mode Exit fullscreen mode

Salve o arquivo e defina a URL de conexão com o banco de dados no arquivo .env e após isso gere a migração e os arquivos do prisma

// criando a migração
pnpm prisma migrate dev --name create-table-users


// gerando o PrismaClient
pnpm prisma generate
Enter fullscreen mode Exit fullscreen mode

O prisma generate irá criar o diretório src/generated/prisma onde ficam os tipos definidos no schema.prisma e o PrismaClient que será usado para fazer as query no banco.

O próximo passo é criar o arquivo src/db.ts para criar uma instância do PrismaClient.

import { PrismaPg } from "@prisma/adapter-pg";
import { env } from "./env.js";
import { PrismaClient } from "./generated/prisma/client.js";

const adapter = new PrismaPg({ connectionString: env.databaseUrl! });
export const db = new PrismaClient({ adapter });
Enter fullscreen mode Exit fullscreen mode

Rotas da aplicação

Vamos criar algumas rotas para testarmos a aplicação. Colocaremos logs para saber o que está acontecendo também.

import { Router } from "express";
import { logger } from "./logger.js";
import { db } from "./db.js";
import { PrismaClientKnownRequestError } from "./generated/prisma/internal/prismaNamespace.js";

const router = Router();

router.get("/", (req, res) => {
  logger.info({ route: "/" }, "Health check endpoint called");

  res.json({ status: "OK" });
});

router.post("/users", async (req, res) => {
  const childLogger = logger.child({ route: "/users", method: "POST" });

  try {
    const { name, email } = req.body;

    childLogger.debug({ data: { name, email } }, "Creating new user");

    const newUser = await db.user.create({
      data: {
        name,
        email
      }
    });

    childLogger.info({ userId: newUser.id }, "New user created");

    res.status(201).json(newUser);
  } catch (error) {
    childLogger.error({ error, route: "/users" }, "Error creating user");

    res.status(500).json({ error: "Internal Server Error" });
  }
});

router.get("/users", async (req, res) => {
  const childLogger = logger.child({ route: "/users", method: "GET" });

  try {
    const users = await db.user.findMany();

    childLogger.info({ userCount: users.length }, "Fetched users successfully");

    res.json(users);

  } catch (error) {
    childLogger.error({ error }, "Error fetching users");

    res.status(500).json({ error: "Internal Server Error" });
  }
});

router.delete("/users/:id", async (req, res) => {
  const childLogger = logger.child({ route: "/users/:id", method: "DELETE" });

  try {
    const userId = Number(req.params.id);

    childLogger.debug({ userId }, "Deleting user");

    await db.user.delete({
      where: { id: userId }
    });

    childLogger.info({ userId }, "User deleted successfully");

    res.status(204).send();
  } catch (error) {
    if (error instanceof PrismaClientKnownRequestError && error.code === "P2025") {
      childLogger.warn({ error }, "User not found for deletion");

      return res.status(404).json({ error: "User not found" });
    }

    childLogger.error({ error }, "Error deleting user");

    res.status(500).json({ error: "Internal Server Error" });
  }
});

export { router };
Enter fullscreen mode Exit fullscreen mode

Servidor

Com tudo isso vamos montar nosso arquivo principal src/server.ts onde iremos criar o servidor e iniciá-lo.

import cors from "cors";
import express, { json } from "express";
import rateLimit from "express-rate-limit";
import helmet from "helmet";
import { env } from "./env.js";
import { logger } from "./logger.js";
import { router } from "./routes.js";

const app = express();

// middleware para parsear JSON
app.use(json());

// middleware de segurança
const oneMinute = 60 * 1000;
app.use(
    helmet(),
    cors({
        origin: "*", // ajuste conforme necessário
        methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
        allowedHeaders: ["Content-Type", "Authorization"]
    }),
    rateLimit({
        windowMs: 15 * oneMinute,
        max: 100
    })
);

// rotas
app.use(router);

app.listen(env.port, () => logger.info(`Server is running on port ${env.port}`));
Enter fullscreen mode Exit fullscreen mode

Scripts

Vamos criar os scripts start, dev e build e vamos definir o arquivo de entrada na chave main do package.json

"main": "dist/server.js",
"scripts": {
    "start": "node --env-file-if-exists=.env .",
    "dev": "tsx --env-file-if-exists=.env src/server.ts",
    "build": "tsc"
  }
Enter fullscreen mode Exit fullscreen mode

Testando

Inicie a aplicação em modo de desenvolvimento com o script dev e vamos iniciar com o nível de log como debug para ver todos os logs que foram criados.

.env

DATABASE_URL="postgresql://dev:dev@localhost:5432/node_prod_post"
NODE_ENV="development"
PORT=3000
LOG_LEVEL="debug"
Enter fullscreen mode Exit fullscreen mode
pnpm dev
Enter fullscreen mode Exit fullscreen mode

Irei fazer as requisições para a API usando a extensão do VSCode REST Client usando um arquivo client.http

@baseUrl = http://localhost:3000

GET {{baseUrl}}

###
POST {{baseUrl}}/users
Content-Type: application/json

{
    "name": "John Doe",
    "email": "john@email.com"
}

###
GET {{baseUrl}}/users

###
DELETE {{baseUrl}}/users/1
Enter fullscreen mode Exit fullscreen mode

Fiz algumas requisições e os logs estão sendo gerados corretamente.

Preparando para produção

O código funciona em ambiente de desenvolvimento. Mas, em produção não teremos código typescript sendo executado ou nenhuma dependência de desenvolvimento instalada.

Primeiro vamos compilar o código para javascript usando o script build. Isso irá gerar uma pasta dist com o código compilado.

Agora vamos definir as variáveis de ambiente que serão usadas em produção que são a URL do banco de produção, o nível de log como "warn" e a NODE_ENV como "production" para ativar otimizações das bibliotecas.

DATABASE_URL="postgresql://dev:dev@localhost:5432/node_prod_post_production"
NODE_ENV="production"
PORT=3000
LOG_LEVEL="warn"
Enter fullscreen mode Exit fullscreen mode

Como eu adicionei uma URL para outro banco de dados, preciso rodar as migrações para gerar as tabelas. Para produção, o prisma tem o comando prisma migrate deploy

pnpm prisma migrate deploy
Enter fullscreen mode Exit fullscreen mode

Por fim, vamos desinstalar as dependências de desenvolvimento.

pnpm i --prod
Enter fullscreen mode Exit fullscreen mode

Agora podemos iniciar o projeto com o script start.

pnpm start
Enter fullscreen mode Exit fullscreen mode

O projeto iniciará silenciosamente no terminal. Como definimos o nível como 'warn', apenas avisos e erros críticos serão exibidos, evitando poluição visual e excesso de uso de disco em produção.

Outra mudança é que os logs são impressos em formato JSON. Embora seja difícil para humanos lerem, esse formato é ideal para ferramentas de monitoramento (como Datadog, CloudWatch ou Elastic Stack) indexarem e processarem as informações.

Um ponto que deve ser levado em conta é que nesse projeto o script start eu usei a flag --env-file-if-exists para usar o arquivo .env para definir as variáveis de ambiente, mas em um ambiente real as variáveis de ambiente não são definidas em um arquivo e sim injetadas diretamente no ambiente.

Conclusão

Com essas configurações, transformamos um simples setup de desenvolvimento em uma aplicação pronta para o mundo real. A diferença entre um projeto amador e um profissional muitas vezes está nesses detalhes: a remoção de dependências desnecessárias, a compilação correta do TypeScript e a estruturação de logs em JSON para ferramentas de observabilidade.

Top comments (0)