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:
# 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 mypyEstrutura de pastas recomendada
Para projetos que vão além de um simples protótipo, recomendo a seguinte organização:
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.tomlEssa 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
# 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] = NoneImplementando o router com operações CRUD
# 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
# 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:
uvicorn app.main:app --reload --port 8000Ao 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
// 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
// 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