Ir para o conteúdo
Linguagem para Dominio1781 palavras

Python para APIs REST: Guia Pratico

Introdução

Se você trabalha com desenvolvimento web há pelo menos dois anos, provavelmente já construiu ou consumiu dezenas de APIs REST. Talvez tenha feito isso com Node.js, Java, C# ou Go. Mas existe uma linguagem que, nos últimos anos, consolidou sua posição como uma das melhores opções para esse domínio: Python.

Python não é apenas a linguagem do machine learning e da ciência de dados. Ela possui um ecossistema maduro, frameworks robustos e uma comunidade gigantesca dedicada ao desenvolvimento de APIs REST de alta performance. Empresas como Netflix, Spotify, Instagram e Uber utilizam Python em partes críticas de suas arquiteturas de backend.

Neste guia, vou mostrar na prática por que Python é uma escolha sólida para APIs REST, quais frameworks e ferramentas utilizar, padrões de arquitetura recomendados e exemplos de código que você pode adaptar para seus projetos. E como muitos de nós trabalhamos em ambientes poliglotas, também incluirei exemplos de como consumir essas APIs a partir de clientes TypeScript, que é uma combinação extremamente comum no mercado.

Por que usar Python para APIs REST

Produtividade e legibilidade

Python é famosa por sua sintaxe limpa. Isso não é apenas uma questão estética: código legível reduz o tempo de onboarding de novos desenvolvedores, facilita code reviews e diminui a incidência de bugs sutis. Em APIs REST, onde a lógica de negócio precisa ser clara e os contratos bem definidos, essa característica faz diferença real.

Ecossistema maduro de frameworks

O ecossistema Python oferece opções para diferentes necessidades:

  • FastAPI: framework moderno, assíncrono, com validação automática via Pydantic e documentação OpenAPI gerada automaticamente. É a escolha número um para novos projetos.
  • Django REST Framework (DRF): ideal para projetos que precisam de um ORM robusto, sistema de autenticação completo e admin panel. Brilha em aplicações com modelos de dados complexos.
  • Flask: minimalista e flexível, perfeito para microserviços simples ou quando você precisa de controle total sobre cada componente.

Tipagem opcional com Pydantic

Uma das maiores críticas históricas ao Python era a falta de tipagem. Com o Pydantic (utilizado nativamente pelo FastAPI), você ganha validação de dados em runtime, serialização automática e documentação de schemas, tudo isso sem perder a flexibilidade da linguagem.

Casos de uso reais onde Python brilha

  • APIs de machine learning serving: quando seu modelo de ML já está em Python, servir predições via API REST com FastAPI elimina a necessidade de reescrita.
  • Plataformas de dados: empresas que precisam de APIs para ingestão e consulta de dados se beneficiam da integração nativa com pandas, SQLAlchemy e bibliotecas de processamento.
  • Backends para aplicações SaaS: o Instagram roda Django em produção servindo centenas de milhões de usuários.
  • Microserviços em arquiteturas poliglotas: Python se integra facilmente via REST com serviços escritos em outras linguagens.

Setup do ambiente de desenvolvimento

Pré-requisitos e instalação

Vamos utilizar FastAPI como framework principal. Aqui está o setup inicial:

Bash
# Crie um ambiente virtual
python -m venv venv
source venv/bin/activate  # Linux/Mac
# venv\Scripts\activate   # Windows
 
# Instale as dependências
pip install fastapi uvicorn pydantic sqlalchemy httpx
 
# Para desenvolvimento
pip install pytest black ruff mypy

Estrutura de pastas recomendada

Para projetos que vão além de um simples protótipo, recomendo a seguinte organização:

Text
project/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── models/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── schemas/
│   │   ├── __init__.py
│   │   └── user.py
│   ├── routers/
│   │   ├── __init__.py
│   │   └── users.py
│   ├── services/
│   │   ├── __init__.py
│   │   └── user_service.py
│   └── repositories/
│       ├── __init__.py
│       └── user_repository.py
├── tests/
├── requirements.txt
└── pyproject.toml

Essa estrutura segue o padrão de separação por camadas (layered architecture), que facilita testes unitários e manutenção a longo prazo.

Exemplo prático: API REST completa com FastAPI

Definindo schemas com Pydantic

Python
# app/schemas/user.py
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
 
 
class UserCreate(BaseModel):
    name: str
    email: EmailStr
    role: str = "viewer"
 
    class Config:
        json_schema_extra = {
            "example": {
                "name": "Maria Silva",
                "email": "[email protected]",
                "role": "editor"
            }
        }
 
 
class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    role: str
    created_at: datetime
    is_active: bool
 
    class Config:
        from_attributes = True
 
 
class UserUpdate(BaseModel):
    name: Optional[str] = None
    email: Optional[EmailStr] = None
    role: Optional[str] = None

Implementando o router com operações CRUD

Python
# app/routers/users.py
from fastapi import APIRouter, HTTPException, Depends, Query
from typing import List
from app.schemas.user import UserCreate, UserResponse, UserUpdate
from app.services.user_service import UserService
 
router = APIRouter(prefix="/api/v1/users", tags=["users"])
 
 
def get_user_service() -> UserService:
    return UserService()
 
 
@router.post("/", response_model=UserResponse, status_code=201)
async def create_user(
    user_data: UserCreate,
    service: UserService = Depends(get_user_service)
):
    existing = await service.find_by_email(user_data.email)
    if existing:
        raise HTTPException(
            status_code=409,
            detail="Já existe um usuário com este email"
        )
    return await service.create(user_data)
 
 
@router.get("/", response_model=List[UserResponse])
async def list_users(
    page: int = Query(1, ge=1, description="Número da página"),
    per_page: int = Query(20, ge=1, le=100, description="Itens por página"),
    role: str = Query(None, description="Filtrar por role"),
    service: UserService = Depends(get_user_service)
):
    return await service.list_users(
        page=page,
        per_page=per_page,
        role=role
    )
 
 
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
    user_id: int,
    service: UserService = Depends(get_user_service)
):
    user = await service.find_by_id(user_id)
    if not user:
        raise HTTPException(status_code=404, detail="Usuário não encontrado")
    return user
 
 
@router.patch("/{user_id}", response_model=UserResponse)
async def update_user(
    user_id: int,
    user_data: UserUpdate,
    service: UserService = Depends(get_user_service)
):
    user = await service.update(user_id, user_data)
    if not user:
        raise HTTPException(status_code=404, detail="Usuário não encontrado")
    return user
 
 
@router.delete("/{user_id}", status_code=204)
async def delete_user(
    user_id: int,
    service: UserService = Depends(get_user_service)
):
    deleted = await service.delete(user_id)
    if not deleted:
        raise HTTPException(status_code=404, detail="Usuário não encontrado")

Arquivo principal da aplicação

Python
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import users
 
app = FastAPI(
    title="API de Gerenciamento de Usuários",
    description="API REST construída com FastAPI",
    version="1.0.0",
    docs_url="/docs",
    redoc_url="/redoc"
)
 
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
 
app.include_router(users.router)
 
 
@app.get("/health")
async def health_check():
    return {"status": "healthy", "version": "1.0.0"}

Para rodar a aplicação:

Bash
uvicorn app.main:app --reload --port 8000

Ao acessar http://localhost:8000/docs, você terá a documentação Swagger completa, gerada automaticamente a partir dos schemas Pydantic e das type hints dos endpoints.

Consumindo a API Python a partir de um cliente TypeScript

Na prática, a maioria dos projetos envolve um frontend ou outro serviço consumindo sua API. Aqui estão dois exemplos práticos em TypeScript que demonstram essa integração.

Exemplo 1: Serviço de consumo da API com tratamento de erros

TYPESCRIPT
// services/userApi.ts
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  created_at: string;
  is_active: boolean;
}
 
interface CreateUserPayload {
  name: string;
  email: string;
  role?: string;
}
 
interface ApiError {
  detail: string;
  status: number;
}
 
class UserApiService {
  private baseUrl: string;
 
  constructor(baseUrl: string = "http://localhost:8000/api/v1") {
    this.baseUrl = baseUrl;
  }
 
  private async handleResponse<T>(response: Response): Promise<T> {
    if (!response.ok) {
      const errorBody = await response.json().catch(() => ({
        detail: "Erro desconhecido na API",
      }));
      const apiError: ApiError = {
        detail: errorBody.detail || "Falha na requisição",
        status: response.status,
      };
      throw apiError;
    }
    return response.json() as Promise<T>;
  }
 
  async createUser(payload: CreateUserPayload): Promise<User> {
    const response = await fetch(`${this.baseUrl}/users/`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    return this.handleResponse<User>(response);
  }
 
  async getUser(userId: number): Promise<User> {
    const response = await fetch(`${this.baseUrl}/users/${userId}`);
    return this.handleResponse<User>(response);
  }
 
  async listUsers(page: number = 1, perPage: number = 20): Promise<User[]> {
    const params = new URLSearchParams({
      page: page.toString(),
      per_page: perPage.toString(),
    });
    const response = await fetch(`${this.baseUrl}/users/?${params}`);
    return this.handleResponse<User[]>(response);
  }
 
  async deleteUser(userId: number): Promise<void> {
    const response = await fetch(`${this.baseUrl}/users/${userId}`, {
      method: "DELETE",
    });
    if (!response.ok) {
      throw { detail: "Falha ao deletar usuário", status: response.status };
    }
  }
}
 
// Uso prático
async function main(): Promise<void> {
  const api = new UserApiService();
 
  try {
    const newUser = await api.createUser({
      name: "João Oliveira",
      email: "[email protected]",
      role: "editor",
    });
    console.log("Usuário criado:", newUser.id);
 
    const users = await api.listUsers(1, 10);
    console.log(`Total de usuários retornados: ${users.length}`);
  } catch (error) {
    const apiError = error as ApiError;
    console.error(`Erro ${apiError.status}: ${apiError.detail}`);
  }
}
 
main();

Exemplo 2: Hook React para integração com a API Python

TYPESCRIPT
// hooks/useUsers.ts
import { useState, useEffect, useCallback, useRef } from "react";
 
interface User {
  id: number;
  name: string;
  email: string;
  role: string;
  created_at: string;
  is_active: boolean;
}
 
interface UseUsersState {
  users: User[];
  loading: boolean;
  error: string | null;
}
 
interface UseUsersReturn extends UseUsersState {
  refetch: () => Promise<void>;
  createUser: (name: string, email: string, role?: string) => Promise<User>;
  removeUser: (userId: number) => Promise<void>;
}
 
const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000/api/v1";
 
type UserChange = (users: User[]) => User[];
 
export function useUsers(page: number = 1): UseUsersReturn {
  const [state, setState] = useState<UseUsersState>({
    users: [],
    loading: true,
    error: null,
  });
 
  const activeRequest = useRef<{ changes: UserChange[] } | null>(null);
 
  const applyChange = useCallback((change: UserChange): void => {
    activeRequest.current?.changes.push(change);
    setState((prev) => ({ ...prev, users: change(prev.users) }));
  }, []);
 
  const fetchUsers = useCallback(async (): Promise<void> => {
    const request = { changes: [] as UserChange[] };
    activeRequest.current = request;
    setState((prev) => ({ ...prev, loading: true, error: null }));
 
    try {
      const params = new URLSearchParams({ page: page.toString() });
      const response = await fetch(`${API_BASE}/users/?${params}`);
 
      if (!response.ok) {
        throw new Error(`Erro ao buscar usuários: ${response.status}`);
      }
 
      const users: User[] = await response.json();
      if (activeRequest.current === request) {
        const nextUsers = request.changes.reduce((current, change) => change(current), users);
        activeRequest.current = null;
        setState({
          users: nextUsers,
          loading: false,
          error: null,
        });
      }
    } catch (err) {
      if (activeRequest.current === request) {
        activeRequest.current = null;
        const message = err instanceof Error ? err.message : "Erro desconhecido";
        setState((prev) => ({ ...prev, loading: false, error: message }));
      }
    }
  }, [page]);
 
  const createUser = useCallback(
    async (name: string, email: string, role: string = "viewer"): Promise<User> => {
      const response = await fetch(`${API_BASE}/users/`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name, email, role }),
      });
 
      if (!response.ok) {
        const body = await response.json();
        throw new Error(body.detail || "Erro ao criar usuário");
      }
 
      const newUser: User = await response.json();
      applyChange((users) => [newUser, ...users.filter((user) => user.id !== newUser.id)]);
      return newUser;
    },
    [applyChange]
  );
 
  const removeUser = useCallback(async (userId: number): Promise<void> => {
    const response = await fetch(`${API_BASE}/users/${userId}`, {
      method: "DELETE",
    });
 
    if (!response.ok) {
      throw new Error("Falha ao remover usuário");
    }
 
    applyChange((users) => users.filter((user) => user.id !== userId));
  }, [applyChange]);
 
  useEffect(() => {
    fetchUsers();
    return () => { activeRequest.current = null; };
  }, [fetchUsers]);
 
  return {
    ...state,
    refetch: fetchUsers,
    createUser,
    removeUser,
  };
}

O hook expõe a lista, o carregamento, o erro da consulta e as ações de recarregar, criar e remover usuários. Apenas a consulta mais recente atualiza esse estado; criações e exclusões concluídas durante essa consulta são preservadas quando a resposta chega. No componente consumidor, trate as rejeições de createUser e removeUser com try/catch, pois essas ações não preenchem o campo error do hook. Configure a URL e confira os contratos da API usada no projeto.

Guias Relacionados

Explore mais guias técnicos

Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.

Ver todos os guias