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
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"
}
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
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
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
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"
}
}
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
tsconfig.json
{
"extends": "@tsconfig/node22/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"noEmitOnError": true
},
"include": ["src"]
}
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"
};
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" }
});
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
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")
}
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
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 });
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 };
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}`));
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"
}
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"
pnpm dev
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
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"
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
Por fim, vamos desinstalar as dependências de desenvolvimento.
pnpm i --prod
Agora podemos iniciar o projeto com o script start.
pnpm start
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)