Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
TutorialIntermediáriocódigo testado

API Node com TypeScript, Express, Prisma e Docker

Crie uma API CRUD completa com Node 24, TypeScript 7, Express 5, Prisma 7, PostgreSQL 18, validação, migração e Docker Compose.

Rodolfo Mori11 min de leitura

Neste tutorial, você vai construir uma API de livraria que cadastra, lista, busca, altera e remove livros. O projeto usa versões testadas de Node, TypeScript, Express, Prisma, PostgreSQL e Docker, com migração de banco, validação de entrada, erros HTTP e imagem de produção.

Não é uma coleção de trechos isolados. O código completo fica em blog/examples/api-livraria, com lockfile e migração, e foi executado de ponta a ponta. A verificação encontrou dois problemas que tutoriais desatualizados costumam esconder: Prisma 7 exige driver adapter e PostgreSQL 18 mudou o ponto de montagem recomendado do volume oficial. A gente vai corrigir ambos no caminho.

Você precisa reconhecer rota, método HTTP, JSON e variável de ambiente. Se essa base ainda for nova, leia o que é uma API REST, o guia de Node e depois volte. O projeto é intermediário porque junta várias peças; cada passo começa pelo problema que resolve.

O balcão, o estoque e o tradutor: o mapa do projeto

Imagine uma livraria física. Express é o balcão: recebe pedidos e devolve respostas. Zod é a ficha de conferência: rejeita pedido sem título ou preço. Prisma é o funcionário que traduz a operação para o banco. PostgreSQL é o estoque registrado. Docker é a planta que permite montar o mesmo conjunto em outra máquina.

Os nomes técnicos são camada HTTP, validação de runtime, ORM, banco relacional e containerização. A analogia mostra responsabilidade, mas não apaga as fronteiras. Prisma não é o banco, Express não valida sozinho e Docker não substitui deploy ou backup.

text
POST /books
  -> Express lê o JSON
    -> Zod valida e normaliza
      -> Prisma Client monta a consulta tipada
        -> adapter-pg usa o driver pg
          -> PostgreSQL grava a linha
            -> Express responde 201 + Location

Quando algo falhar, esse desenho ajuda a perguntar em qual balcão o pedido parou. Erro 400 nasce da entrada; 404, da ausência do recurso; conexão recusada acontece antes de a consulta chegar ao estoque.

1. Crie a pasta e fixe as versões

bash
mkdir api-livraria
cd api-livraria
npm init -y
bash
npm install express@5.2.1 zod@4.4.3 dotenv@17.2.3 @prisma/client@7.9.1 @prisma/adapter-pg@7.9.1 pg@8.23.0
npm install --save-dev prisma@7.9.1 typescript@7.0.2 tsx@4.23.12 @types/express@5.0.6 @types/node@24.10.13

Versão fixa torna o tutorial repetível. Num projeto mantido, ferramentas de atualização mostram correções posteriores; você atualiza uma família por vez, roda testes e lê migrações. @latest em um texto eterno pode entregar outra API meses depois.

Edite os campos centrais do package.json:

json
{
  "name": "api-livraria-devclub",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": {
    "dev": "tsx watch src/server.ts",
    "build": "tsc -p tsconfig.json",
    "start": "node dist/server.js",
    "db:generate": "prisma generate",
    "db:migrate": "prisma migrate dev",
    "db:deploy": "prisma migrate deploy",
    "check": "tsc -p tsconfig.json --noEmit"
  }
}

type: module deixa o projeto em ESM. dev executa TypeScript durante o desenvolvimento; build gera JavaScript; start executa apenas a saída pronta.

2. Configure TypeScript para Node ESM

json
{
  "compilerOptions": {
    "target": "ES2023",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "sourceMap": true
  },
  "include": ["src/**/*.ts"]
}

strict faz o editor perguntar antes de você adivinhar. As duas opções seguintes apertam arrays e propriedades opcionais. Elas causaram erros reais durante a construção e revelaram pontos em que undefined podia chegar à consulta.

Em ESM com NodeNext, imports relativos escritos no TypeScript terminam em .js, mesmo apontando para um arquivo .ts durante o build:

ts
import { app } from './app.js';

Parece estranho porque é novo: TypeScript encontra o fonte correspondente e emite um import que o Node entende depois. O guia de TypeScript aprofunda cada opção.

bash
npm run check

Nenhuma saída significa nenhum erro de tipo.

3. Suba somente o PostgreSQL

Crie compose.yaml primeiro com o banco:

yaml
services:
  db:
    image: postgres:18-alpine
    ports:
      - "55432:5432"
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: devclub
      POSTGRES_DB: livraria
    volumes:
      - postgres-data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d livraria"]
      interval: 3s
      timeout: 3s
      retries: 10

volumes:
  postgres-data:

O lado 55432 evita disputar a porta local padrão; dentro da rede o banco continua em 5432. A imagem PostgreSQL 18 deve montar o volume em /var/lib/postgresql. Usar o antigo /var/lib/postgresql/data fez o contêiner 18.6 abortar no teste, com uma mensagem que protege a organização por versão.

Inicie e espere o healthcheck:

bash
docker compose up -d --wait db
docker compose ps
NAME IMAGE SERVICE STATUS api-livraria-db-1 postgres:18-alpine db Up (healthy)

O arquivo .env conecta ferramentas executadas no host:

bash
PORT=3000
DATABASE_URL=postgresql://app:devclub@localhost:55432/livraria

Adicione .env ao .gitignore e publique somente .env.example. A senha deste tutorial é descartável. Produção usa segredo gerenciado e uma credencial por aplicação.

4. Modele o livro no Prisma 7

Crie prisma/schema.prisma:

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

datasource db {
  provider = "postgresql"
}

model Book {
  id         Int      @id @default(autoincrement())
  title      String
  author     String
  priceCents Int      @map("price_cents")
  stock      Int      @default(0)
  createdAt  DateTime @default(now()) @map("created_at")
  updatedAt  DateTime @updatedAt @map("updated_at")

  @@index([title])
  @@map("books")
}

Prisma 7 usa o gerador prisma-client e exige output explícito. @map mantém o código em camelCase e o banco em snake_case. priceCents é inteiro: R$ 79,90 vira 7990, evitando arredondamento de ponto flutuante.

A URL saiu do schema.prisma. Ela mora em prisma.config.ts:

ts
import 'dotenv/config';
import { defineConfig, env } from 'prisma/config';

export default defineConfig({
  schema: 'prisma/schema.prisma',
  migrations: { path: 'prisma/migrations' },
  datasource: { url: env('DATABASE_URL') },
});

Essa é uma diferença do Prisma 7 que exemplos antigos não mostram. Gere a migração:

bash
npm run db:migrate -- --name init
Applying migration `..._init`

The following migration(s) have been created and applied: prisma/migrations/ └─ …_init/ └─ migration.sql

Your database is now in sync with your schema.

Migração é histórico versionado da estrutura, não botão para sincronizar tudo. Leia o SQL gerado antes de enviar ao Git:

sql
CREATE TABLE "books" (
  "id" SERIAL NOT NULL,
  "title" TEXT NOT NULL,
  "author" TEXT NOT NULL,
  "price_cents" INTEGER NOT NULL,
  "stock" INTEGER NOT NULL DEFAULT 0,
  "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  "updated_at" TIMESTAMP(3) NOT NULL,
  CONSTRAINT "books_pkey" PRIMARY KEY ("id")
);

O guia de PostgreSQL explica tabela, índice e transação por baixo do ORM.

5. Conecte Prisma Client pelo adapter-pg

Crie src/db.ts:

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

const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
  throw new Error('DATABASE_URL não foi definida');
}

const adapter = new PrismaPg({ connectionString });
export const prisma = new PrismaClient({ adapter });

No Prisma 7, conexão direta exige adapter. PrismaPg liga o Client gerado ao driver pg. O erro cedo para URL ausente é mais útil que uma falha obscura na primeira requisição.

Não crie um novo PrismaClient dentro de cada rota. O pool de conexões pertence à aplicação. Ambientes serverless podem exigir estratégia do provedor, mas um servidor Node persistente reutiliza a instância.

6. Valide entrada com Zod

TypeScript não valida JSON vindo da rede. Quando a aplicação executa, o tipo foi apagado. src/schemas.ts cria a catraca de runtime:

ts
import { z } from 'zod';

export const idSchema = z.coerce.number().int().positive();

export const createBookSchema = z.object({
  title: z.string().trim().min(2).max(120),
  author: z.string().trim().min(2).max(100),
  priceCents: z.number().int().nonnegative(),
  stock: z.number().int().nonnegative().default(0),
});

export const updateBookSchema = createBookSchema.partial().refine(
  (data) => Object.keys(data).length > 0,
  { message: 'envie pelo menos um campo' },
);

coerce converte o parâmetro de URL, que chega como texto. Para o body, o contrato exige número JSON de verdade; não converte "7990" silenciosamente. partial permite alteração de um campo e refine rejeita objeto vazio.

7. Monte o Express e a rota de saúde

Comece src/app.ts:

ts
import express from 'express';
import { z } from 'zod';
import { Prisma } from './generated/prisma/client.js';
import { prisma } from './db.js';
import { createBookSchema, idSchema, updateBookSchema } from './schemas.js';

export const app = express();
app.disable('x-powered-by');
app.use(express.json({ limit: '32kb' }));

O limite evita aceitar body ilimitado para registros pequenos. Desativar o cabeçalho não é barreira de segurança, apenas reduz informação desnecessária.

A saúde confirma processo e banco:

ts
app.get('/health', async (_request, response) => {
  await prisma.$queryRaw`SELECT 1`;
  response.json({ status: 'ok' });
});

$queryRaw com template parametrizado é seguro para esse SQL fixo. Não troque por uma versão insegura concatenando entrada.

8. Liste e busque livros

A rota GET /books aceita ?search=:

ts
app.get('/books', async (request, response) => {
  const search = typeof request.query.search === 'string'
    ? request.query.search.trim()
    : '';

  const books = await prisma.book.findMany({
    ...(search
      ? { where: { title: { contains: search, mode: 'insensitive' as const } } }
      : {}),
    orderBy: { id: 'asc' },
  });

  response.json({ data: books, total: books.length });
});

O spread condicional não é charme. Com exactOptionalPropertyTypes, enviar where: undefined não equivale a omitir where. A primeira versão fez o TypeScript 7 interromper o build; construir o objeto sem a propriedade resolveu sem enfraquecer o compilador.

Para uma base grande, acrescente paginação e reveja o índice: busca contains insensível não usa automaticamente um índice B-tree simples. ORM não elimina plano de consulta.

9. Crie e leia um recurso

Criação responde 201 Created e informa a nova URL:

ts
app.post('/books', async (request, response) => {
  const input = createBookSchema.parse(request.body);
  const book = await prisma.book.create({ data: input });

  response
    .status(201)
    .location(`/books/${book.id}`)
    .json({ data: book });
});

Busca por id diferencia dado ausente de erro interno:

ts
app.get('/books/:id', async (request, response) => {
  const id = idSchema.parse(request.params.id);
  const book = await prisma.book.findUnique({ where: { id } });

  if (!book) {
    return response.status(404).json({ error: 'livro não encontrado' });
  }

  return response.json({ data: book });
});

/books/banana vira 400, pois o identificador tem formato inválido. /books/999 válido mas inexistente vira 404. Essa precisão ajuda o front a decidir o que mostrar.

10. Atualize sem enviar undefined ao Prisma

Valide e construa Prisma.BookUpdateInput explicitamente:

ts
app.patch('/books/:id', async (request, response) => {
  const id = idSchema.parse(request.params.id);
  const input = updateBookSchema.parse(request.body);

  const data: Prisma.BookUpdateInput = {};
  if (input.title !== undefined) data.title = input.title;
  if (input.author !== undefined) data.author = input.author;
  if (input.priceCents !== undefined) data.priceCents = input.priceCents;
  if (input.stock !== undefined) data.stock = input.stock;

  const book = await prisma.book.update({ where: { id }, data });
  response.json({ data: book });
});

A versão ingênua passava o objeto parcial direto e falhou com exactOptionalPropertyTypes. A correção torna visível quais campos a API permite alterar. Em domínios sensíveis, essa lista também evita mass assignment.

Remoção responde sem body:

ts
app.delete('/books/:id', async (request, response) => {
  const id = idSchema.parse(request.params.id);
  await prisma.book.delete({ where: { id } });
  response.status(204).send();
});

11. Traduza erros num único middleware

Express 5 encaminha rejeições de handlers assíncronos ao middleware de erro. Use a assinatura com quatro argumentos:

ts
app.use((error: unknown, _request, response, _next) => {
  if (error instanceof z.ZodError) {
    return response.status(400).json({
      error: 'dados inválidos',
      fields: error.issues.map((issue) => ({
        path: issue.path.join('.'),
        message: issue.message,
      })),
    });
  }

  if (
    error instanceof Prisma.PrismaClientKnownRequestError &&
    error.code === 'P2025'
  ) {
    return response.status(404).json({ error: 'livro não encontrado' });
  }

  console.error(error);
  return response.status(500).json({ error: 'erro interno' });
});

O cliente não recebe stack trace, SQL ou segredo. O servidor registra o erro inesperado. Produção deve adicionar id de correlação e logger estruturado; console.error é o degrau mínimo.

12. Inicie e encerre sem cortar conexões

src/server.ts carrega .env, abre a porta e trata sinais:

ts
import 'dotenv/config';
import { app } from './app.js';
import { prisma } from './db.js';

const port = Number(process.env.PORT ?? 3000);
const server = app.listen(port, '0.0.0.0', () => {
  console.log(`API da livraria em http://localhost:${port}`);
});

async function shutdown(signal: string) {
  console.log(`encerrando por ${signal}`);
  server.close(async () => {
    await prisma.$disconnect();
    process.exit(0);
  });
}

process.on('SIGTERM', () => void shutdown('SIGTERM'));
process.on('SIGINT', () => void shutdown('SIGINT'));

0.0.0.0 aceita a conexão encaminhada pelo Docker. No desligamento, o servidor para de aceitar trabalho e desconecta o banco depois de fechar.

bash
npm run db:generate
npm run check
npm run build
npm start

13. Prove o CRUD com curl

Confira saúde:

bash
curl -i http://localhost:3000/health
HTTP/1.1 200 OK content-type: application/json; charset=utf-8

{“status”:“ok”}

Cadastre:

bash
curl -i -X POST http://localhost:3000/books -H 'content-type: application/json' -d '{"title":"JavaScript por Inteiro","author":"Ana Lima","priceCents":7990,"stock":12}'
HTTP/1.1 201 Created location: /books/1

{“data”:{“id”:1,“title”:“JavaScript por Inteiro”,“author”:“Ana Lima”,“priceCents”:7990,“stock”:12,…}}

Busque e altere:

bash
curl 'http://localhost:3000/books?search=javascript'
curl -X PATCH http://localhost:3000/books/1 -H 'content-type: application/json' -d '{"stock":10}'

Agora provoque o erro de propósito:

bash
curl -i -X POST http://localhost:3000/books -H 'content-type: application/json' -d '{"title":"A"}'
HTTP/1.1 400 Bad Request

{“error”:“dados inválidos”,“fields”:[ {“path”:“title”,“message”:“Too small: expected string to have >=2 characters”}, {“path”:“author”,“message”:“Invalid input: expected string, received undefined”}, {“path”:“priceCents”,“message”:“Invalid input: expected number, received undefined”} ]}

O erro é parte do contrato, não acidente escondido. O front consegue associar path ao campo correto.

Remova e confirme o 404:

bash
curl -i -X DELETE http://localhost:3000/books/1
curl -i http://localhost:3000/books/1
HTTP/1.1 204 No Content

HTTP/1.1 404 Not Found {“error”:“livro não encontrado”}

14. Construa uma imagem sem ferramentas de desenvolvimento

O Dockerfile usa dois estágios:

dockerfile
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json prisma.config.ts ./
COPY prisma ./prisma
COPY src ./src
RUN DATABASE_URL=postgresql://build:build@localhost:5432/build npm run db:generate && npm run build

FROM node:24-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev --omit=optional && npm cache clean --force
COPY --from=build /app/dist ./dist
COPY --from=build /app/prisma ./prisma
COPY --from=build /app/prisma.config.ts ./prisma.config.ts
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

A URL falsa existe apenas durante prisma generate, que carrega a configuração mas não conecta. Nenhum segredo real entra na imagem. O estágio final instala somente runtime; --omit=optional também impede que o CLI Prisma, dependência opcional do Client, chegue à API.

O comando npm audit --omit=dev --omit=optional retornou zero vulnerabilidades no runtime testado. O estágio de build reportou um advisory transitivo no CLI Prisma 7.9.1 sem correção compatível naquela data. Não usamos audit fix --force, que sugeria voltar para Prisma 6 e quebraria o projeto; isolamos a ferramenta do runtime e registramos o risco para atualizar quando o upstream corrigir.

Isso é uma decisão prática de segurança: scanner não é semáforo que autoriza mudança incompatível. Você identifica pacote, alcance, código que chega à produção, mitigação e prazo de revisão.

15. Una migração, API e banco com Compose

Complete compose.yaml:

yaml
services:
  api:
    build: .
    environment:
      PORT: 3000
      DATABASE_URL: postgresql://app:devclub@db:5432/livraria
    ports:
      - "3010:3000"
    depends_on:
      migrate:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').then(r => { if (!r.ok) process.exit(1) }).catch(() => process.exit(1))"]
      interval: 5s
      timeout: 3s
      retries: 10

  migrate:
    build:
      context: .
      target: build
    command: ["npx", "prisma", "migrate", "deploy"]
    environment:
      DATABASE_URL: postgresql://app:devclub@db:5432/livraria
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: devclub
      POSTGRES_DB: livraria
    volumes:
      - postgres-data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d livraria"]
      interval: 3s
      timeout: 3s
      retries: 10

volumes:
  postgres-data:

Dentro da rede, db é o endereço do PostgreSQL; localhost apontaria para a própria API. O serviço migrate usa o estágio com ferramentas, termina com sucesso e só então libera a API. A imagem final continua sem compilador e CLI.

Suba tudo:

bash
docker compose up -d --build --wait
docker compose ps
curl http://localhost:3010/health
api Up db Up (healthy)

{“status”:“ok”}

O build real gerou Prisma Client 7.9.1, compilou TypeScript 7.0.2, aplicou a migração no PostgreSQL 18.6 e iniciou a API como usuário node. O POST no contêiner respondeu 201 e a busca retornou a linha persistida.

Para parar sem apagar dados:

bash
docker compose down

docker compose down -v também remove o volume. Use somente quando tiver confirmado que o banco é descartável ou possui backup.

16. Teste o sistema vazio, usado e reiniciado

Executar um POST uma vez prova o caminho feliz, não o projeto inteiro. A unidade real deste tutorial é API mais migração mais banco. O teste que dá confiança começa com um banco vazio, aplica o histórico versionado e percorre o contrato HTTP como um cliente faria.

Pense numa vistoria de apartamento. Olhar a torneira aberta não confirma ralo, registro, vazamento e retorno da água depois de uma manutenção. Cada cenário força uma parte do encanamento. Na API, os cenários mínimos são saúde, criação, consulta, validação, ausência, alteração, remoção e reinício.

Crie um projeto Compose separado para o teste, para nunca apontar sem querer ao banco de desenvolvimento:

bash
docker compose -p livraria-teste up -d --build --wait
curl -f http://localhost:3010/health

O nome após -p isola rede, contêineres e volume. Em uma esteira concorrente, publique a API numa porta dinâmica ou execute as chamadas de dentro da rede, evitando que dois jobs disputem 3010.

Monte uma tabela de casos antes de automatizar:

Caso Ação Resultado esperado
banco pronto GET /health 200 e status: ok
livro válido POST /books 201, Location e id
body incompleto POST /books 400 e campos inválidos
id inválido GET /books/abc 400
id ausente GET /books/999 404
estoque parcial PATCH /books/1 200 e demais campos intactos
remoção DELETE /books/1 204
recurso removido GET /books/1 404

Automação pode usar o test runner do Node e uma biblioteca de requisição, mas preserve a observação externa. Testar diretamente uma função do controller não prova parser JSON, rota, middleware de erro nem status.

O segundo grupo de testes verifica persistência. Cadastre um livro, recrie somente a API e busque o mesmo id. Depois execute docker compose down, suba novamente sem -v e repita a busca. O dado deve continuar porque mora no volume, não na camada gravável do contêiner.

O terceiro grupo verifica evolução. Acrescente uma migração que permita ISBN nulo, aplique num banco com livros existentes e só depois passe a exigir ISBN na API. Mudança compatível em etapas evita que código novo dependa de coluna ainda ausente ou que migração rígida falhe nos dados antigos.

Há uma regra de ouro: migrate dev cria e ajusta migrações no ambiente de desenvolvimento; migrate deploy aplica arquivos já revisados no ambiente de entrega. Produção não deve inventar migração interativamente.

Finalize limpando apenas o projeto de teste:

bash
docker compose -p livraria-teste down -v

O -v está autorizado aqui porque o nome identifica um laboratório descartável. Não transforme esse comando em script apontado para qualquer projeto. Backup e restauração precisam de um exercício separado com dados que importam.

Quando automatizar, guarde logs de migração, versão da imagem e respostas que falharam. “Job vermelho” informa pouco; um artefato com etapa, status e corpo reduz o tempo até a causa.

17. Checklist antes de chamar de produção

Este projeto ensina a fundação, não finge encerrar operação. Antes de publicar:

  • autentique e autorize cada ação;
  • substitua credenciais didáticas por segredos gerenciados;
  • defina CORS para origens conhecidas quando houver navegador;
  • adicione rate limit e limite de conexão;
  • registre método, rota, status, duração e id de correlação;
  • crie testes de integração num banco isolado;
  • execute migração numa etapa controlada, com backup e retorno planejado;
  • configure TLS, domínio, healthcheck e encerramento gradual;
  • acompanhe erros, latência, pool e consultas lentas;
  • teste restauração do backup, não apenas a criação dele.

O guia de Docker aprofunda imagem, volume e segurança. O guia de Prisma acompanha schema, relações e migrações além deste CRUD.

Missão: transforme a livraria em um catálogo seu

Adicione isbn único e uma rota GET /books/:id. Depois tente cadastrar o mesmo ISBN duas vezes. Seu middleware deve traduzir o erro conhecido do Prisma para 409 Conflict, sem mostrar SQL.

O critério de sucesso tem cinco provas:

  1. npm run check termina sem erro;
  2. a migração contém coluna e índice único;
  3. o primeiro POST responde 201;
  4. o segundo responde 409 com JSON estável;
  5. docker compose down seguido de up preserva o primeiro livro.

Se você consegue apontar qual camada produz cada resultado, explicar por que o adapter existe e reconstruir tudo em outra máquina, deixou de copiar uma API e passou a entender o sistema.

  • api node
  • typescript
  • express
  • prisma
  • postgresql
  • docker
  • crud

Perguntas frequentes

Posso fazer este tutorial sem Docker?
Sim. Instale PostgreSQL 18, crie usuário e banco e ajuste DATABASE_URL. O código Node é o mesmo. Docker torna a versão do banco e o ambiente final reproduzíveis; Prisma não depende dele.
Por que o tutorial usa Prisma 7 e não Prisma 8?
Prisma 7.9.1 era a versão estável GA durante a verificação. Prisma 8 ainda aparecia como release candidate. O tutorial não ensina uma prévia como fundação de produção.
Por que o Prisma 7 precisa de adapter-pg?
Conexões diretas no Prisma 7 exigem um driver adapter. Para PostgreSQL, o PrismaPg liga o client gerado ao driver pg. Isso substitui exemplos antigos que criavam PrismaClient sem adapter.
Esta API está pronta para receber tráfego público?
Ela tem validação, erros, migração, usuário sem root e desligamento limpo, mas produção ainda exige autenticação, autorização, rate limit, observabilidade, TLS, backup, gestão de segredos e testes de integração.
Por que o preço é armazenado em centavos?
Dinheiro não deve depender de ponto flutuante. Neste projeto, 79,90 reais vira o inteiro 7990. Sistemas com múltiplas moedas e precisão variável podem usar decimal com regras explícitas.
Onde está o projeto completo do tutorial?
Ele fica em blog/examples/api-livraria no mesmo repositório. A pasta preserva Dockerfile, Compose, lockfile, migração e código executável exatamente como foram validados.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.19.0, TypeScript 7.0.2, Express 5.2.1, Prisma 7.9.1, PostgreSQL 18.6, Docker 29.5.3 e Compose 5.1.4, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. Node.js Docs — Environment variables — nodejs.org
  2. TypeScript Handbook — typescriptlang.org
  3. Express 5 — Migrating — expressjs.com
  4. Prisma ORM 7 — Get started — prisma.io
  5. npm — versões do Prisma — npmjs.com
  6. Prisma ORM — Database drivers — prisma.io
  7. PostgreSQL 18 — Tutorial — postgresql.org
  8. Docker Docs — Node.js guide — docs.docker.com

Continue por aqui