O Primeiro GET Endpoint
Com a configuração resolvida, vamos implementar as camadas de baixo para cima: banco de dados, repository, service, controller e route.
Conexão com o banco
import { Pool } from "pg";
import { loadConfig } from "./config";
const config = loadConfig();
export const pool = new Pool({ connectionString: config.databaseUrl });
Código 5-8: Pool de conexões com o PostgreSQL.
Usamos Pool em vez de Client aqui. Um Client abre uma única conexão com o
banco — se duas requisições chegarem ao mesmo tempo, uma precisa esperar a outra
terminar. O Pool mantém várias conexões abertas e distribui entre as
requisições. Para o migrate.ts, que roda uma vez e sai, Client é adequado.
Para um servidor que atende requisições continuamente, Pool.
Repository
import { pool } from "../db";
export async function findAll() {
const result = await pool.query(`
SELECT
e.id,
e.title,
e.description,
e.location,
e.date,
e.capacity,
e.created_at,
u.name AS organizer_name
FROM events e
JOIN users u ON e.organizer_id = u.id
ORDER BY e.date ASC
`);
return result.rows;
}
Código 5-9: Repository que busca todos os eventos no banco.
A query já faz o JOIN com users para trazer o nome do organizador — o frontend
precisa exibir essa informação, e fazer duas consultas separadas seria
desperdício. O ORDER BY e.date ASC retorna os eventos em ordem cronológica.
Service
import * as eventsRepository from "./events.repository";
export async function listAll() {
return eventsRepository.findAll();
}
Código 5-10: Service que (por enquanto) apenas delega ao repository.
Nesse momento, o service parece inútil — ele apenas repassa a chamada para o repository. A camada existe porque é aqui que lógica de negócio vai morar quando aparecer. Filtrar eventos por data, verificar permissões de acesso — tudo isso entra no service sem mexer no repository nem no controller.
Controller
import { Request, Response, NextFunction } from "express";
import * as eventsService from "./events.service";
export async function getAll(req: Request, res: Response, next: NextFunction) {
try {
const events = await eventsService.listAll();
res.json(events);
} catch (err) {
next(err);
}
}
Código 5-11: Controller que responde com a lista de eventos em JSON.
O controller chama o service, formata a resposta (o res.json serializa o array
para JSON e define o header Content-Type: application/json automaticamente) e,
se der erro, repassa para o middleware de tratamento de erros via next(err).
Route
import { Router } from "express";
import * as eventsController from "./events.controller";
const router = Router();
router.get("/api/events", eventsController.getAll);
export default router;
Código 5-12: Rota que mapeia GET /api/events ao controller.
A rota é a camada mais fina: apenas conecta uma URL e um método HTTP a uma função do controller.
Montando tudo no app.ts
import "dotenv/config";
import express from "express";
import eventsRoutes from "./events/events.routes";
import { errorHandler } from "./errors/errorHandler";
import { loadConfig } from "./config";
const config = loadConfig();
const app = express();
app.use(eventsRoutes);
app.use(errorHandler);
app.listen(config.port, () => {
console.log(`Servidor rodando em http://localhost:${config.port}`);
});
Código 5-13: Ponto de entrada do servidor.
O import "dotenv/config" na primeira linha carrega as variáveis do arquivo
.env para process.env. Precisa estar antes de qualquer código que leia
variáveis de ambiente — inclusive antes do loadConfig(). O dotenv procura o
arquivo .env no diretório de onde o processo foi executado, por isso todos os
comandos a seguir usam cd backend antes de rodar o servidor.
Testando o endpoint
Inicie o servidor e faça uma requisição com curl:
$ cd backend
$ npx tsx src/app.ts
Servidor rodando em http://localhost:3000
Em outro terminal:
$ curl -s http://localhost:3000/api/events | jq '.[0]'
{
"id": 1,
"title": "Hackathon UFS",
"description": "Maratona de programação de 24h aberta a todos os cursos",
"location": "Centro de Vivência - UFS",
"date": "2026-09-15T11:00:00.000Z",
"capacity": 50,
"created_at": "2026-08-21T14:32:07.412Z",
"organizer_name": "Ana Silva"
}
O primeiro evento do TamoJunto, servido como JSON. O frontend vai consumir exatamente essa resposta no próximo capítulo.
Para conferir que todos os 12 eventos estão lá:
$ curl -s http://localhost:3000/api/events | jq 'length'
12