Projeto Final: API com Postgres + Redis
2 min de leitura
fonte
Você percorreu SQL (Postgres via Prisma) e NoSQL (Redis como acelerador). Agora é hora de juntar os dois numa API real: dados de negócio no Postgres, performance e sessão no Redis.
Esse projeto reaproveita o brief de
api-rest-com-autenticacao
e adiciona a camada Redis que faltava.
O que você vai construir
Uma API Express com:
- CRUD de produtos (criar, listar, atualizar, deletar) com persistência em Postgres via Prisma.
- Cache de leitura:
GET /produtos/:idconsulta Redis antes do banco; invalida no update. - Sessão de login no Redis (em vez de JWT stateless) com revogação por logout.
- Rate limit simples: cada IP pode fazer 60 requests por minuto.
- Healthcheck em
/healthque checa Postgres e Redis. - Docker Compose com
app + postgres + redis, tudo sobe com um comando.
Estrutura de pastas sugerida
src/
├── server.ts
├── app.ts
├── routes/
│ ├── produtos.ts
│ └── auth.ts
├── lib/
│ ├── prisma.ts # singleton do PrismaClient
│ ├── redis.ts # singleton do ioredis
│ ├── cache.ts # helpers get/set/invalidate
│ └── ratelimit.ts # middleware de rate limit
├── middleware/
│ ├── auth.ts # lê sessão do Redis
│ └── error.ts
└── types.ts # declare global Express.Request
prisma/
└── schema.prisma # model Produto, model Usuario
docker-compose.yml # app + postgres + redis
As três peças de Redis que você vai implementar
1. Cache com invalidação:
// lib/cache.ts
export async function getCached<T>(key: string): Promise<T | null> {
const data = await redis.get(key);
return data ? JSON.parse(data) : null;
}
export async function setCache(key: string, value: unknown, ttl = 3600) {
await redis.set(key, JSON.stringify(value), "EX", ttl);
}
export async function invalidate(key: string) {
await redis.del(key);
}
// routes/produtos.ts
app.get("/produtos/:id", async (req, res) => {
const id = Number(req.params.id);
const cacheKey = `produto:${id}`;
const cached = await getCached<Produto>(cacheKey);
if (cached) return res.json(cached); // cache hit
const produto = await prisma.produto.findUnique({ where: { id } });
if (!produto) return res.status(404).json({ erro: "não encontrado" });
await setCache(cacheKey, produto);
res.json(produto);
});
app.put("/produtos/:id", async (req, res) => {
const id = Number(req.params.id);
const produto = await prisma.produto.update({
where: { id },
data: req.body,
});
await invalidate(`produto:${id}`); // invalida cache
res.json(produto);
});
2. Sessão no Redis (em vez de JWT):
// routes/auth.ts
app.post("/login", async (req, res) => {
const { email, senha } = req.body;
const usuario = await prisma.usuario.findUnique({ where: { email } });
if (!usuario || !(await bcrypt.compare(senha, usuario.senhaHash))) {
return res.status(401).json({ erro: "credenciais inválidas" });
}
const sid = crypto.randomUUID();
await redis.set(
`sessao:${sid}`,
JSON.stringify({ usuarioId: usuario.id }),
"EX", 60 * 60 * 24 // 1 dia
);
res.cookie("sid", sid, { httpOnly: true, sameSite: "lax" });
res.json({ ok: true });
});
3. Rate limit:
// lib/ratelimit.ts
export async function rateLimit(ip: string, maxPorMinuto: number) {
const key = `ratelimit:${ip}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
return count <= maxPorMinuto;
}
app.use(async (req, res, next) => {
const ip = req.ip ?? "unknown";
if (!(await rateLimit(ip, 60))) {
return res.status(429).json({ erro: "muitas requisições" });
}
next();
});
Critérios de aceite
A API está pronta quando:
docker compose upsobe app + Postgres + Redis, e a aplicação conecta nos dois.GET /produtos/:idna primeira chamada lê do Postgres; na segunda, vem do Redis (verificável comredis-cliou log estruturado).PUT /produtos/:idinvalida o cache - próxima leitura vai no banco de novo.POST /logincria sessão no Redis;POST /logoutremove; requests autenticadas funcionam só com sessão válida.- 61ª request no mesmo minuto devolve 429.
/healthconfirma Postgres e Redis respondendo.
O que esse projeto testa de NoSQL
- Quando usar embed vs reference (aqui não se aplica - é SQL com cache, não modelagem de documento).
- Cache com invalidação manual - o padrão mais comum.
- Sessão com TTL no Redis - alternativa ao JWT stateless.
- Rate limit com
INCR+EXPIRE- atômico, simples, barato. - Healthcheck dos dois bancos - boa prática de produção.
Se você chegou aqui, você sabe quando usar cada banco - e mais importante, sabe quando não usar.
// recursos
// avaliação da trilha
—
ainda sem avaliações