
Construindo uma API RESTful em Python com FastAPI
Quando você começa a criar APIs, muitos tutoriais pulam direto para o código. Entretanto, entender por que cada decisão é tomada torna tudo mais simples de manter, testar e evoluir. Este é um artigo para quem já tem uma noção de programação em Python mas, se você está iniciando sua jornada com a programação em Python, poderá ler esses dois artigos antes, onde damos os primeiros passos em Python e depois entendemos melhor criando nosso primeiro programa. Mas se você já estiver pronto, vamos adiante!
Então, vamos construir uma API de Produtos passo a passo, com camadas bem definidas, validação robusta e documentação automática.
Se você está começando no mundo das APIs, este guia foi feito especialmente para você. Assim, vamos construir uma API RESTful de Produtos com Python, usando o framework FastAPI e um banco de dados SQLite. Por fim, você entenderá o que é um CRUD como API, como cada camada se encaixa e como testar sua aplicação com segurança.
Ao final, terá um projeto completo e funcional, pronto para evoluir com autenticação, filtros e testes automatizados.
O que é uma API RESTful e como o CRUD se encaixa nela?
Antes de começar a codar, é importante entender o conceito.
Uma API RESTful é uma interface que permite que sistemas diferentes conversem entre si usando o protocolo HTTP — o mesmo da web. Ela se baseia em padrões claros: recursos, métodos e status.
Por exemplo, se o recurso é “produtos”, nossa API terá URLs como:
GET /produtos
POST /produtos
PUT /produtos/{id}
DELETE /produtos/{id}
Essas quatro operações formam o famoso CRUD, que representa:
| Letra | Ação | Descrição |
|---|---|---|
| C | Create | Criar registros (POST) |
| R | Read | Ler/listar registros (GET) |
| U | Update | Atualizar registros (PUT/PATCH) |
| D | Delete | Excluir registros (DELETE) |
Ao longo deste artigo, cada parte do código será associada a uma dessas letras.
Assim, você saberá claramente em que etapa do CRUD está trabalhando. Mas, se você prefere antes construir uma aplicação com essas operações para entender primeiro a camada de persistência, leia antes esse artigo sobre CRUD com Python.
O que você vai construir
Uma API RESTful de Produtos com:
- Camadas claras: Rotas (FastAPI) → Serviço (regras) → Repositório (SQLite).
- Contratos HTTP padronizados: JSON, status codes e cabeçalhos previsíveis.
- Validação de dados: feita automaticamente pelo Pydantic.
- Documentação interativa: Swagger/OpenAPI e permitindo teste dos endpoints pelo navegador.
- CRUD completo: criar, listar, atualizar e excluir produtos persistindo em SQLite.
Preparando o ambiente
Para começar, você precisa do Python instalado. Então, se não tiver o Pyton ainda instalado, siga o artigo sobre instalação de Python.
Depois, abra o terminal e crie um ambiente virtual isolado — isso evita conflitos de dependências entre projetos. Se desejar saber mais sobre isso, veja esse artigo sobre ambientes de execução Python isolados.
python -m venv venv
Ative o ambiente:
- No Linux/macOS:
source venv/bin/activate - No Windows (PowerShell):
venv\Scripts\activate
Com o ambiente ativo, instale as bibliotecas que usaremos:
pip install fastapi uvicorn pydantic
O fastapi é o framework principal.
O uvicorn é o servidor ASGI que executará a aplicação.
E o pydantic é a biblioteca que validará automaticamente os dados das requisições.
💡 Dica: se preferir, crie um arquivo requirements.txt e coloque dentro:
fastapi
uvicorn
pydantic
Depois, execute:
pip install -r requirements.txt
Assim, fica mais fácil compartilhar o projeto com outras pessoas.
Testando requisições: Terminal, cURL ou Postman
Durante o desenvolvimento, você poderá testar as rotas da API de várias maneiras:
- Usando o navegador: o FastAPI já traz o Swagger em
/docs. - Usando o terminal: com o comando
curl(nativo no Linux e macOS). - Usando o Postman: ideal para quem está no Windows ou prefere uma interface gráfica.
O Postman é gratuito e permite salvar coleções de requisições, ajustar cabeçalhos e visualizar respostas de forma organizada.
Estrutura do projeto
Vamos organizar nosso projeto desta forma:
api_produtos/
├─ main.py # Rotas (FastAPI)
├─ models.py # Modelos Pydantic (validação de entrada e saída)
├─ database.py # Conexão com SQLite
├─ repository.py # Repositório (operações SQL)
└─ services.py # Regras de negócio (camada de serviço)
Cada camada tem uma função clara:
- Rotas: expõem endpoints HTTP.
- Serviço: contém regras de negócio.
- Repositório: acessa o banco de dados.
- Modelos (Pydantic): validam e estruturam os dados.
Por que FastAPI?
FastAPI é um framework moderno, rápido e intuitivo. Esse artigo só apresenta uma “beiradinha” do que ele é capaz, mas será ótimo para você ter uma noção se ainda não o conhece!
Esse framework também usa type hints para validar automaticamente seus dados e gerar documentação interativa.
Portanto, ele:
- Integra o Pydantic de forma nativa, simplificando contratos.
- Valida automaticamente seus dados com base nos modelos.
- Gera documentação OpenAPI (Swagger em
/docse Redoc em/redoc). - Roda sobre ASGI, oferecendo excelente desempenho com pouco boilerplate.
Assim, você escreve menos código, com mais segurança e velocidade.
Onde entra o Pydantic?
O Pydantic define os “Models” ou modelos de dados (esquemas) de validação. Desse modo, ele:
- Garante tipos corretos (
str,float,int…). - Aplica regras de negócio simples (ex.:
preco > 0). - Gera schemas JSON aproveitados pelo Swagger.
Consequentemente, erros são barrados na borda da API e os contratos ficam padronizados.
Sobre os “Models”: eles são iguais aos de um MVC?
Essa é uma dúvida comum — e muito válida.
No MVC (Model-View-Controller) tradicional, o Model representa a entidade de negócio e também é responsável por se comunicar com o banco de dados.
Já no FastAPI, usamos dois tipos de Model com responsabilidades diferentes:
- Modelos Pydantic: descrevem como os dados entram e saem da API (validação e serialização).
- Modelos do domínio (ou ORM): representam a entidade no banco (quando usamos ORM, como SQLAlchemy).
Mesmo parecendo duplicidade, essa separação é intencional. Porque, ela garante isolamento entre camadas, ou seja, se o banco mudar, a API continua com os mesmos contratos externos. Portanto, o sistema fica mais robusto e fácil de manter.
Criando o CRUD de Produtos passo a passo
Agora vamos então construir nosso projeto. Cada parte será associada à letra correspondente do CRUD.
1. Modelos — models.py (base do domínio e validação de dados)
Essa parte, como dito ali acima, define como os dados chegam e saem da API. Então crie o arquivo models.py e insira esse código:
from pydantic import BaseModel, Field, field_validator
from typing import Optional
class ProdutoBase(BaseModel):
nome: str = Field(..., example="Mouse sem fio")
preco: float = Field(..., gt=0, example=99.90)
descricao: Optional[str] = Field(None, example="Mouse óptico USB com alta precisão")
@field_validator("nome")
def nome_nao_vazio(cls, v):
if not v or not v.strip():
raise ValueError("nome não pode ser vazio")
return v.strip()
class ProdutoCriar(ProdutoBase):
pass
class ProdutoAtualizar(ProdutoBase):
pass
class Produto(ProdutoBase):
id: int
Observe que aproveitamos para definiros tipos que extendem da classe base. Fazemos isso para separar e especificar os tipos conforme as suas responsabilidades.
2. Banco de dados — database.py
Nessa etapa, podemos já começar definindo um jeito padrão de “criamos e gerenciamos a conexão com o SQLite”. Então crie o arquivo database.py e adicione esse código:
import sqlite3
from pathlib import Path
from contextlib import contextmanager
CAMINHO_BANCO = Path("produtos.sqlite") # Definimos aqui uma constante com o caminho do arquivo do banco de dados.
# essa função sempre criará nossa conexão com banco de dados
def obter_conexao():
con = sqlite3.connect(CAMINHO_BANCO, check_same_thread=False)
con.row_factory = sqlite3.Row
return con
@contextmanager
def conexao_gerenciada():
con = obter_conexao()
try:
yield con
con.commit()
except:
con.rollback()
raise
finally:
con.close()
def inicializar_banco():
with conexao_gerenciada() as con:
con.execute("""
CREATE TABLE IF NOT EXISTS produtos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nome TEXT NOT NULL,
preco REAL NOT NULL,
descricao TEXT
);
""")
Este código configura uma maneira robusta e segura de interagir com um banco de dados SQLite (produtos.sqlite). Ele define como obter a conexão, usa um gerenciador de contexto (conexao_gerenciada) para garantir que a conexão seja sempre aberta, comitada (ou desfeita) e fechada corretamente, e possui uma função para criar a tabela inicial de produtos.
Observação: row_factory=sqlite3.Row facilita transformar linhas em dict
3. Repositório — repository.py
Aqui implementamos as operações diretas no banco de dados (sem regras de negócio). Veja que temos todos os métodos de recuperação e persistência de dados.
from typing import List, Optional
from models import Produto
from database import conexao_gerenciada
class RepositorioProduto:
# R READ
def listar(self) -> List[Produto]:
with conexao_gerenciada() as con:
cur = con.execute("SELECT * FROM produtos ORDER BY id")
return [Produto(**dict(row)) for row in cur.fetchall()]
# R READ
def obter_por_id(self, produto_id: int) -> Optional[Produto]:
with conexao_gerenciada() as con:
cur = con.execute("SELECT * FROM produtos WHERE id = ?", (produto_id,))
row = cur.fetchone()
return Produto(**dict(row)) if row else None
# C - CREATE
def criar(self, nome: str, preco: float, descricao: Optional[str]) -> Produto:
with conexao_gerenciada() as con:
cur = con.execute(
"INSERT INTO produtos (nome, preco, descricao) VALUES (?, ?, ?)",
(nome, preco, descricao),
)
novo_id = cur.lastrowid
return Produto(id=novo_id, nome=nome, preco=preco, descricao=descricao)
# U - UPDATE
def atualizar(self, produto_id: int, nome: str, preco: float, descricao: Optional[str]) -> Optional[Produto]:
with conexao_gerenciada() as con:
cur = con.execute("""
UPDATE produtos
SET nome = ?, preco = ?, descricao = ?
WHERE id = ?
""", (nome, preco, descricao, produto_id))
if cur.rowcount == 0:
return None
return Produto(id=produto_id, nome=nome, preco=preco, descricao=descricao)
# D - DELETE
def excluir(self, produto_id: int) -> bool:
with conexao_gerenciada() as con:
cur = con.execute("DELETE FROM produtos WHERE id = ?", (produto_id,))
return cur.rowcount > 0
Note que repositório não decide regras; ele apenas lê/escreve. Portanto, seu teste é direto e seu uso é previsível.
4. Serviço — services.py
Agora que temos o respoitorio, precisamos de serviços. Um Serviço centraliza as regras de negócio e o tratamento de erros. Além disso, de forma simples, rota fala com o serviço, que fala com o repositório.
from typing import List
from models import Produto, ProdutoCriar, ProdutoAtualizar
from repository import RepositorioProduto
class ErroDeNegocio(Exception):
"""Erros de domínio/regra de negócio."""
class ServicoProduto:
def __init__(self, repositorio: RepositorioProduto):
self.repositorio = repositorio
def listar(self) -> List[Produto]:
return self.repositorio.listar()
def obter(self, produto_id: int) -> Produto:
produto = self.repositorio.obter_por_id(produto_id)
if not produto:
raise ErroDeNegocio("produto_nao_encontrado")
return produto
def criar(self, dto: ProdutoCriar) -> Produto:
return self.repositorio.criar(dto.nome, dto.preco, dto.descricao)
def atualizar(self, produto_id: int, dto: ProdutoAtualizar) -> Produto:
atualizado = self.repositorio.atualizar(produto_id, dto.nome, dto.preco, dto.descricao)
if not atualizado:
raise ErroDeNegocio("produto_nao_encontrado")
return atualizado
def excluir(self, produto_id: int) -> None:
ok = self.repositorio.excluir(produto_id)
if not ok:
raise ErroDeNegocio("produto_nao_encontrado")
Detalhe Importante: Onde Colocamos as Regras?
Em nosso projeto, optamos por uma abordagem mais simples:
- A Camada de Serviço é onde colocamos as regras de domínio mais básicas (o “como” e “porquê” das ações).
Mas, saiba que…
Em arquiteturas mais avançadas (como a Hexagonal ou Arquitetura Limpa), as regras de negócio são isoladas nas próprias Entidades de Domínio e Objetos de Valor. Isso é feito para:
- Isolar estrategicamente cada parte.
- Garantir melhor manutenção e testabilidade.
Para Você Agora:
Não se preocupe com a complexidade dessas arquiteturas mais avançadas por enquanto. Mantenha o foco no Serviço.
Dica para Depuração:
Se um erro de execução ocorrer (e não for erro de digitação), olhar para a Camada de Serviço — como ela invoca seus métodos e quem ela chama (ex: o Repositório) — é o seu melhor ponto de partida para encontrar e corrigir o problema.
5. Rotas — main.py
Agoras por meio das rotas, definimos os endpoints HTTP da API usando FastAPI. Então crie o arquivo main.py e adicione esté código:
from fastapi import FastAPI, HTTPException, Depends
from typing import List
from models import Produto, ProdutoCriar, ProdutoAtualizar
from database import inicializar_banco
from repository import RepositorioProduto
from services import ServicoProduto, ErroDeNegocio
app = FastAPI(
title="API de Produtos — FastAPI + Serviço + Repositório",
version="1.0.0",
description="Exemplo didático de API RESTful com SQLite e camadas",
)
def obter_servico_produto() -> ServicoProduto:
return ServicoProduto(RepositorioProduto())
@app.on_event("startup")
def ao_iniciar():
inicializar_banco()
@app.get("/", tags=["Raiz"])
def raiz():
return {"mensagem": "API de Produtos — consulte /docs para testar no Swagger"}
@app.get("/produtos", response_model=List[Produto], tags=["Produtos"])
def listar_produtos(servico: ServicoProduto = Depends(obter_servico_produto)):
return servico.listar()
@app.get("/produtos/{produto_id}", response_model=Produto, tags=["Produtos"])
def obter_produto(produto_id: int, servico: ServicoProduto = Depends(obter_servico_produto)):
try:
return servico.obter(produto_id)
except ErroDeNegocio:
raise HTTPException(status_code=404, detail="Produto não encontrado")
@app.post("/produtos", response_model=Produto, status_code=201, tags=["Produtos"])
def criar_produto(dto: ProdutoCriar, servico: ServicoProduto = Depends(obter_servico_produto)):
return servico.criar(dto)
@app.put("/produtos/{produto_id}", response_model=Produto, tags=["Produtos"])
def atualizar_produto(produto_id: int, dto: ProdutoAtualizar, servico: ServicoProduto = Depends(obter_servico_produto)):
try:
return servico.atualizar(produto_id, dto)
except ErroDeNegocio:
raise HTTPException(status_code=404, detail="Produto não encontrado")
@app.delete("/produtos/{produto_id}", status_code=204, tags=["Produtos"])
def excluir_produto(produto_id: int, servico: ServicoProduto = Depends(obter_servico_produto)):
try:
servico.excluir(produto_id)
except ErroDeNegocio:
raise HTTPException(status_code=404, detail="Produto não encontrado")
Executando o servidor
Agora já temos um CRUD completo como API e que pode operar sobre nosso banco de dados. Então, para testar, primeiro rode o comando:
uvicorn main:app --reload

O parâmetro --reload recarrega automaticamente a API sempre que você salvar um arquivo.
Por padrão, ela ficará disponível em:
👉 http://127.0.0.1:8000
E a documentação Swagger em:
👉 http://127.0.0.1:8000/docs

Testando a API
Você pode testar:
- No Swagger do próprio navegador.
- No Postman.
- Ou usando cURL pelo terminal (como mostramos antes).
Cada teste representa uma das operações do CRUD:
Criar (POST), Ler (GET), Atualizar (PUT) e Excluir (DELETE).
Testar no Swagger (recomendado)
Acesse http://127.0.0.1:8000/docs. Em seguida, clique no endpoint → Try it out → preencha → Execute.
Você verá request, response, status e schema — tudo claro e interativo.
Por exemplo, vá até POST / produtos e clique em Try it out:

Observe que um campo com o corpo da requisição é exibido com valores de exemplo, que definimos lá no início ao construir a Model do Pydantic. Sendo assim, você pode clicar em ¨ Execute” e esses dados serão sumetidos. Ou ainda, poderá primeiro alterá-los antes de executar e por fim “executar”.

Depois de executar, você vai perceber uma resposta imediata logo abaixo com o id do registro criado. Além disso, observe que lá no terminal, onde você iniciou o uvicorn, um log é apresentado mostrando cada solicitação feita ao servidor e suas respostas.

você também deve ter notado a criação do arquivo produtos.sqlite no diretório do projeto.

Testes rápidos por cURL
Criar:
curl -X POST http://127.0.0.1:8000/produtos \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"nome": "Cadeira Gamer", "preco": 1299.99, "descricao": "Ergonômica e reclinável"}'

Listar:
curl -H "Accept: application/json" http://127.0.0.1:8000/produtos
Atualizar:
curl -X PUT http://127.0.0.1:8000/produtos/1 \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"nome": "Cadeira Gamer Pro", "preco": 1499.90, "descricao": "Novo encosto lombar"}'
Excluir:
curl -X DELETE -i http://127.0.0.1:8000/produtos/1
Conclusão
Com poucos arquivos e uma estrutura bem pensada, você acabou de criar uma API RESTful completa com FastAPI.
Mais importante: agora entende o papel de cada camada e como elas se relacionam.
Separar responsabilidades pode parecer repetição, mas é o que permite um código mais limpo e sustentável.
Se o dia de amanhã exigir trocar o banco de dados ou adicionar autenticação, você já terá uma base sólida para isso.
Veja também
- CRUD em Python com SQLite — passo a passo para iniciantes
- Seu primeiro programa em Python
- Como instalar o Python no seu computador
- Entendendo variáveis de ambiente e isolamento com Pipenv
