Destino Dev

Blog

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_limit conforme 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