API REST com NestJS, Prisma e PostgreSQL: JWT, testes e otimização de performance
Por John Pires · 06 de março de 2026

Contexto
Projetos backend modernos exigem mais do que endpoints que funcionam: precisam ser seguros, testáveis e responsáveis por alto throughput. Este artigo aborda, em nível avançado, como montar uma API REST com NestJS usando Prisma como ORM para PostgreSQL, autenticação com JWT, testes automatizados (unitários e end-to-end) e técnicas práticas de otimização de performance para produção.
Vamos pressupor um serviço de gerenciamento de usuários e autenticação. O foco não é repetir a teoria básica, mas mostrar decisões arquiteturais, código real e armadilhas comuns.
Explicação técnica
Stack e responsabilidades
Resumo das responsabilidades de cada peça:
- NestJS: estrutura modular, injeção de dependência, estratégia de controllers/providers e middleware/guard para autenticação.
- Prisma: mapeamento do schema para consultas tipadas, migrations e geração de cliente para acesso ao PostgreSQL.
- PostgreSQL: armazenamento relacional, índices, consultas otimizadas e recursos como prepared statements.
- JWT: transporte compacto de identidade (bearer token) utilizado para autenticar requests sem estado.
- Testes: Jest + SuperTest para e2e; testes unitários para services e guards usando mocks do Prisma.
Autenticação JWT: onde falham implementações comuns
JWT é simples, mas os problemas aparecem em produção: tokens longos demais, ausência de rotação/revogação, uso indevido do algoritmo, armazenamento inseguro no cliente e falta de verificação de 'jti' ou 'exp' corretamente. Regras práticas:
- Assine tokens com uma chave forte (preferir keys no KMS em produção).
- Use short-lived access tokens + refresh tokens com armazenamento seguro (httpOnly cookies ou armazenados no cliente com cuidados).
- Inclua 'iat', 'exp' e 'jti'; mantenha uma tabela de revogação para refresh tokens.
- No Guard, valide o token completo e recupere claims mínimos para evitar reconsultas desnecessárias ao DB.
Prisma e modelagem para performance
Modelar tabelas pensando em consultas comuns reduz I/O. Exemplos de decisões:
- Indexar campos usados em WHERE e JOIN (email, userId, createdAt para ordenação).
- Evitar selects "select *"; use select/include do Prisma para buscar apenas campos necessários.
- Usar relacionamentos com includes controlados; para listagens em massa, prefira consultas com joins paginadas.
- Use conexões e configuração de pool adequada no datasource do Prisma para evitar exaustão de conexões.
Testes automatizados: estratégia e isolamento
Testes que valem na produção cobrem três níveis:
- Unitários: services e guards isolados com mocks do Prisma Client (jest.mock ou injeção de providers falsos).
- Integração: interações com o banco real em um ambiente controlado, usando um banco ephemeral (testcontainers ou bases de testes dedicadas).
- E2E: instanciar o Nest app e usar SuperTest para validar rotas, autenticação e contractos HTTP.
Regra prática: evite usar o mesmo banco de desenvolvimento para testes. Use uma instância temporária ou schema isolado e 'prisma migrate reset' entre runs.
Performance: foco nas amostras que importam
Técnicas testadas em produção:
- Paginação cursor-based para grandes listas (cursor + limit) ao invés de offset para manter consistência e desempenho.
- Projeção de campos via select para reduzir payload e I/O.
- Batching e caching de leituras com DataLoader para reduzir N+1 em includes feitos na aplicação.
- Cache de resultados imutáveis ou pouco mutáveis no Redis com TTL estratégico.
- Prepared statements com pooling no PostgreSQL via PgBouncer para reduzir overhead de parsing/plan.
- Monitoramento de queries lentas e uso de EXPLAIN ANALYZE para otimizar índices.
Exemplos
A seguir três exemplos práticos: 1) Auth service com JWT e refresh token básico; 2) schema Prisma e consulta otimizada; 3) teste e2e com Jest + SuperTest.
1) auth.service.ts (NestJS) — foco em geração/validação JWT e refresh token
import { Injectable, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { PrismaClient } from '@prisma/client'
import * as bcrypt from 'bcrypt'
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService, private prisma = new PrismaClient()) {}
async validateUserByPassword(email: string, password: string) {
const user = await this.prisma.user.findUnique({ where: { email }, select: { id: true, passwordHash: true, role: true } })
if (!user) return null
const match = await bcrypt.compare(password, user.passwordHash)
if (!match) return null
return { id: user.id, role: user.role }
}
async login(user: { id: string, role: string }) {
const payload = { sub: user.id, role: user.role }
const accessToken = this.jwtService.sign(payload, { expiresIn: '15m' })
const refreshToken = this.jwtService.sign({ sub: user.id }, { expiresIn: '7d', secret: process.env.REFRESH_TOKEN_SECRET })
// Save hashed refresh token in DB for revocation
const hashed = await bcrypt.hash(refreshToken, 10)
await this.prisma.refreshToken.create({ data: { userId: user.id, tokenHash: hashed, expiresAt: new Date(Date.now() + 7 * 24 * 3600 * 1000) } })
return { accessToken, refreshToken }
}
async refresh(refreshToken: string) {
try {
const payload = this.jwtService.verify(refreshToken, { secret: process.env.REFRESH_TOKEN_SECRET })
const stored = await this.prisma.refreshToken.findFirst({ where: { userId: payload.sub }, orderBy: { createdAt: 'desc' } })
if (!stored) throw new UnauthorizedException()
const match = await bcrypt.compare(refreshToken, stored.tokenHash)
if (!match) throw new UnauthorizedException()
const user = await this.prisma.user.findUnique({ where: { id: payload.sub }, select: { id: true, role: true } })
return this.login({ id: user.id, role: user.role })
} catch (e) {
throw new UnauthorizedException()
}
}
}
2) schema.prisma e consulta otimizada
// schema.prisma
datasource db {
provider = 'postgresql'
url = env('DATABASE_URL')
}
generator client {
provider = 'prisma-client-js'
}
model User {
id String @id @default(uuid())
email String @unique
passwordHash String
role String
createdAt DateTime @default(now())
posts Post[]
}
model Post {
id String @id @default(uuid())
authorId String
author User @relation(fields: [authorId], references: [id])
title String
content String
createdAt DateTime @default(now())
@@index([authorId])
}
// Consulta paginada cursor-based com Prisma
const pageSize = 20
async function listPosts(cursor?: string) {
const where = {}
const posts = await prisma.post.findMany({
take: pageSize + 1,
...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}),
orderBy: { createdAt: 'desc' },
select: { id: true, title: true, createdAt: true, author: { select: { id: true, email: true } } }
})
const hasMore = posts.length > pageSize
if (hasMore) posts.pop()
return { data: posts, nextCursor: hasMore ? posts[posts.length - 1].id : null }
}
3) e2e test com Jest + SuperTest
import { INestApplication } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'
describe('Auth e2e', () => {
let app: INestApplication
beforeAll(async () => {
const module = await Test.createTestingModule({ imports: [AppModule] }).compile()
app = module.createNestApplication()
await app.init()
})
afterAll(async () => {
await app.close()
})
it('should login and access protected route', async () => {
const login = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@example.com', password: 'password' })
.expect(201)
const token = login.body.accessToken
await request(app.getHttpServer())
.get('/users/me')
.set('Authorization', `Bearer ${token}`)
.expect(200)
})
})
Erros comuns
Aqui estão falhas práticas observadas em projetos reais e como evitá-las.
1. Não controlar o número de conexões do Prisma
Problema: em deploys serverless ou muitas réplicas, conexões explodem e o PostgreSQL fica sem recursos. Solução: setar pool no datasource, usar PgBouncer em transaction pooling, reduzir maxConnections por instância e usar lazy connections.
2. Fazer includes excessivos
Problema: includes recursivos ou muitos relacionamentos retornam payloads enormes e aumentam tempos. Solução: projetar select/include por endpoint, criar DTOs e mapear manualmente quando necessário.
3. Confiar apenas em JWT sem estratégia de revogação
Problema: tokens vazados continuam válidos até o expiry. Solução: short-lived access tokens + refresh tokens revogáveis salvos no DB (hash) e checados no refresh. Para logout, deletar o refresh token.
4. Não testar cenários de concorrência
Problema: operações de saldo ou contadores falham sob concorrência. Solução: use transações do Prisma (prisma.$transaction) com SELECT FOR UPDATE em queries críticas, ou leve a lógica atômica para o banco com stored procedures quando necessário.
5. Logar dados sensíveis
Problema: debug logs com tokens ou senhas. Solução: sanitize logs, configure log levels por ambiente e use ferramentas de observabilidade que ofusquem dados sensíveis.
Checklist final
- Arquitetura: módulos NestJS claros (auth, users, prisma module) e injeção do Prisma Client via provider.
- JWT: chave forte, short-lived access tokens, refresh tokens armazenados com hash e expirations.
- Prisma: migrations versionadas, schema otimizado com índices, select/include controlados.
- Pooling: PgBouncer em produção ou configurar datasource
connection_limitconforme tráfego. - Paginação: cursor-based para listas grandes; evitar offset para performance e consistência.
- Caching: Redis para leituras caras; validar TTL e invalidação em escrita (pattern: cache-aside).
- Batching: DataLoader para resolver N+1 quando resolver entidades relacionadas em massa.
- Transações: use prisma.$transaction para operações multi-step críticas; prefira atomicidade no banco.
- Testes: cobrir services com testes unitários, integrações com banco isolado e e2e com SuperTest.
- Monitoramento: habilitar query logging em staging e usar EXPLAIN ANALYZE para queries lentas.
Resumo prático de implantação: build do Nest em container leve; usar variáveis de ambiente para segredos; usar KMS para chaves JWT; PgBouncer para pooling; Redis para cache; e um pipeline de testes que roda unitários + integração antes do deploy.
Implementar bem esses pontos reduz bugs críticos e melhora a escalabilidade. A combinação NestJS + Prisma + PostgreSQL é poderosa, mas exige disciplina em modelagem, controle de conexões, testes e gestão de tokens para produção robusta.
Assuntos: NestJS, Prisma, PostgreSQL, jwt, Performance, tests
Leia também
Autenticação JWT segura no NestJS com refresh tokens rotativos e Redis
Implementação prática de autenticação JWT no NestJS com refresh tokens rotativos, armazenamento de jti em Redis e detecção de replay. Guia com exemplos de…
Autorização por permissões no NestJS usando decorators e guards
Aprenda a controlar autorização por permissões no NestJS usando decorators e guards. Tutorial prático, passo a passo, com exemplos funcionais, integração JWT…
Implementando Circuit Breaker e Bulkhead em microservices NestJS com Cockatiel
Guia prático para integrar circuit breaker e bulkhead com Cockatiel em NestJS: módulo centralizado, wrapper Http resiliente, tratamento de erros e otimizações…
NestJS: criar e testar microservice TCP e gateway HTTP passo a passo
Passo a passo prático para criar e testar um microservice TCP em NestJS e um gateway HTTP. Mostro o jeito errado, o jeito certo, exemplos funcionais e como…
Rate limiting por usuário no NestJS com Guard + Redis (sliding window)
Aprenda a implementar rate limiting por usuário no NestJS usando Guard + Redis com sliding window. Guia passo a passo, exemplos práticos (Lua + ioredis)…
Interceptor global de logs e tempo no NestJS: implementar e testar
Implemente um interceptor global no NestJS para logs e medição de tempo, registre-o no bootstrap e teste com Jest e supertest. Artigo prático passo a passo…