Projeto Prático — API REST com TypeScript
Aula 7 de 7
Setup do Projeto
mkdir ts-api && cd ts-api
npm init -y
# Dependências
npm install express zod @prisma/client
npm install --save-dev typescript @types/express tsx prisma vitest
# Inicializar TypeScript
npx tsc --init --strict --target ES2022 --module ESNext --moduleResolution bundler --outDir dist --rootDir src
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true,
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
},
"include": ["src"]
}
Schemas com Zod + Tipagem
// src/schemas/usuario.ts
import { z } from "zod";
export const criarUsuarioSchema = z.object({
nome: z.string().min(2, "Nome deve ter no mínimo 2 caracteres"),
email: z.string().email("Email inválido"),
idade: z.number().int().positive().optional(),
});
export const atualizarUsuarioSchema = criarUsuarioSchema.partial();
// Inferir tipos dos schemas
export type CriarUsuarioDTO = z.infer<typeof criarUsuarioSchema>;
export type AtualizarUsuarioDTO = z.infer<typeof atualizarUsuarioSchema>;
Tipagem de Request/Response
// src/types/express.d.ts
import "express";
declare module "express" {
interface Request {
usuario?: {
id: number;
nome: string;
};
}
}
DTOs e Validação
// src/middlewares/validacao.ts
import { Request, Response, NextFunction } from "express";
import { ZodSchema, ZodError } from "zod";
export function validar(schema: ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
try {
req.body = schema.parse(req.body);
next();
} catch (erro) {
if (erro instanceof ZodError) {
res.status(400).json({
erro: "Dados inválidos",
detalhes: erro.errors.map((e) => ({
campo: e.path.join("."),
mensagem: e.message,
})),
});
return;
}
next(erro);
}
};
}
Controller com Tipos Genéricos
// src/controllers/UsuarioController.ts
import { Request, Response } from "express";
import { prisma } from "../lib/prisma";
import type { CriarUsuarioDTO, AtualizarUsuarioDTO } from "../schemas/usuario";
export class UsuarioController {
async listar(req: Request, res: Response): Promise<void> {
const usuarios = await prisma.usuario.findMany();
res.json(usuarios);
}
async buscarPorId(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const usuario = await prisma.usuario.findUnique({
where: { id: Number(id) },
});
if (!usuario) {
res.status(404).json({ erro: "Usuário não encontrado" });
return;
}
res.json(usuario);
}
async criar(req: Request, res: Response): Promise<void> {
const dados: CriarUsuarioDTO = req.body;
const usuario = await prisma.usuario.create({ data: dados });
res.status(201).json(usuario);
}
async atualizar(req: Request, res: Response): Promise<void> {
const { id } = req.params;
const dados: AtualizarUsuarioDTO = req.body;
const usuario = await prisma.usuario.update({
where: { id: Number(id) },
data: dados,
});
res.json(usuario);
}
async deletar(req: Request, res: Response): Promise<void> {
const { id } = req.params;
await prisma.usuario.delete({ where: { id: Number(id) } });
res.status(204).send();
}
}
Rotas Tipadas
// src/routes/usuario.ts
import { Router } from "express";
import { UsuarioController } from "../controllers/UsuarioController";
import { validar } from "../middlewares/validacao";
import {
criarUsuarioSchema,
atualizarUsuarioSchema,
} from "../schemas/usuario";
const router = Router();
const controller = new UsuarioController();
router.get("/", controller.listar.bind(controller));
router.get("/:id", controller.buscarPorId.bind(controller));
router.post("/", validar(criarUsuarioSchema), controller.criar.bind(controller));
router.put("/:id", validar(atualizarUsuarioSchema), controller.atualizar.bind(controller));
router.delete("/:id", controller.deletar.bind(controller));
export default router;
Server
// src/server.ts
import express from "express";
import usuarioRoutes from "./routes/usuario";
const app = express();
app.use(express.json());
app.use("/api/usuarios", usuarioRoutes);
app.listen(3000, () => {
console.log("Servidor rodando em http://localhost:3000");
});
export default app;
Prisma ORM Tipado
npx prisma init
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Usuario {
id Int @id @default(autoincrement())
nome String
email String @unique
idade Int?
criadoEm DateTime @default(now())
atualizadoEm DateTime @updatedAt
}
// src/lib/prisma.ts
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
npx prisma generate
npx prisma db push
Testes com Vitest
// src/__tests__/schemas/usuario.test.ts
import { describe, it, expect } from "vitest";
import { criarUsuarioSchema } from "../../schemas/usuario";
describe("criarUsuarioSchema", () => {
it("deve validar dados corretos", () => {
const dados = {
nome: "Alice",
email: "[email protected]",
idade: 30,
};
const resultado = criarUsuarioSchema.parse(dados);
expect(resultado).toEqual(dados);
});
it("deve rejeitar email inválido", () => {
const dados = {
nome: "Alice",
email: "invalido",
};
expect(() => criarUsuarioSchema.parse(dados)).toThrow();
});
it("deve rejeitar nome muito curto", () => {
const dados = {
nome: "A",
email: "[email protected]",
};
expect(() => criarUsuarioSchema.parse(dados)).toThrow();
});
});
// src/__tests__/controllers/usuario.test.ts
import { describe, it, expect, vi } from "vitest";
import { Request, Response } from "express";
import { UsuarioController } from "../../controllers/UsuarioController";
describe("UsuarioController", () => {
it("deve listar usuários", async () => {
const controller = new UsuarioController();
const req = {} as Request;
const res = {
json: vi.fn(),
} as unknown as Response;
await controller.listar(req, res);
expect(res.json).toHaveBeenCalled();
});
});
Build e Deploy
# Desenvolvimento
npm run dev # tsx watch src/server.ts
# Build
npm run build # tsc
# Produção
npm start # node dist/server.js
# Type checking
npm run typecheck # tsc --noEmit
# Testes
npm test # vitest run
{
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "tsc",
"start": "node dist/server.js",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest"
}
}
Lab: Executar o Projeto
# Clonar ou criar o projeto
npm install
npx prisma generate
npx prisma db push
# Iniciar servidor
npm run dev
# Testar endpoints
curl http://localhost:3000/api/usuarios
curl -X POST http://localhost:3000/api/usuarios \
-H "Content-Type: application/json" \
-d '{"nome":"Alice","email":"[email protected]","idade":30}'
# Rodar testes
npm test
TypeScript + Zod + Prisma formam um trio imbatível para APIs type-safe, com tipos fluindo do banco à resposta HTTP sem perder informações.