kb.erickguedes.com
TypeScript: Tipagem e Produtividade

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.