Quando eu comecei a juntar FastAPI com Postgres, o objetivo era bem simples: criar um backend rápido, limpo e fácil de manter, sem virar um quebra-cabeça na hora de conectar no banco. Essa combinação costuma ficar bem estável porque o FastAPI é ótimo para rotas e validação, e o Postgres é forte e confiável para dados.
O caminho mais tranquilo, pra mim, foi usar SQLModel, que é uma biblioteca que junta duas coisas: o SQLAlchemy (camada de acesso a banco via Python) e o Pydantic (validação de dados). A ideia é escrever modelos que viram tabelas e, ao mesmo tempo, servem pra validar entrada e saída das rotas.
Estrutura do projeto e o que cada arquivo resolve
Pra não misturar responsabilidade, eu gosto de separar a configuração, o banco e os modelos em arquivos diferentes. Isso deixa o código mais previsível e diminui gambiarra quando o projeto cresce. Também facilita testes e manutenção, porque cada parte faz uma coisa só. No básico, costuma existir um arquivo de configuração, um arquivo do banco e uma pasta de modelos. As rotas e serviços ficam em módulos separados pra evitar um main.py gigante.
Uma estrutura simples e bem comum fica assim: config.py para ler variáveis, db.py para criar engine e sessão, models/ para tabelas, e main.py pra subir a aplicação. O termo engine é a “máquina” de conexão do SQLAlchemy/SQLModel, que sabe como falar com o banco. Já a session é a sessão de transação, usada pra consultar, inserir e confirmar alterações. Separando isso, o restante do app só pede uma session pronta e trabalha.
Conectando com segurança usando variáveis de ambiente (.env)
Eu evito colocar usuário e senha do banco direto no código. O jeito mais comum é usar um arquivo .env, que guarda configurações sensíveis fora do código fonte. A “string de conexão” é um texto que descreve como acessar o Postgres: usuário, senha, host, porta e nome do banco. Com isso, trocar o banco de dev para produção vira só trocar valores, sem editar Python. Além disso, esse padrão combina bem com deploy e containers.
Antes dos códigos, vale entender que o pydantic_settings lê essas variáveis e valida tipos. E o lru_cache guarda em memória a configuração carregada, pra não reler o arquivo toda hora. A seguir está um exemplo do conteúdo do .env e um config.py completo para carregar a conexão.
Este exemplo mostra um .env mínimo com a string de conexão do Postgres.
# .env
PG_CONNECTION_STRING="postgresql://usuario:senha@localhost:5432/meu_banco"
Este exemplo mostra um config.py que carrega o .env e expõe get_settings() para usar no app inteiro.
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
PG_CONNECTION_STRING: str
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
@lru_cache()
def get_settings() -> Settings:
return Settings()
Checando a configuração no FastAPI (exemplo simples)
Antes de mexer com banco de verdade, eu gosto de validar se a configuração está chegando. Isso evita perder tempo debugando erro de conexão quando, na real, a variável nem está sendo lida. No FastAPI, dá pra criar uma rota só pra conferir se a configuração está acessível. Não é algo pra ficar em produção, mas ajuda no começo. Também deixa claro como importar e usar o get_settings().
Nesse exemplo, a rota retorna a string de conexão, o que é útil só em ambiente local. Em um cenário real, expor isso seria um risco, porque mostra credenciais. Mesmo assim, como demonstração didática, ajuda a provar que o pipeline .env → Settings → app está funcionando. A seguir vai um main.py mínimo com essa rota.
Este exemplo mostra um main.py simples usando get_settings() dentro de uma rota.
from fastapi import FastAPI
from config import get_settings
app = FastAPI()
@app.get("/pg-connection-string")
async def pg_connection_string():
pg_connection_string = get_settings().PG_CONNECTION_STRING
return {"pg_connection_string": pg_connection_string}
Modelos com SQLModel: tabelas e validação no mesmo lugar
No SQLModel, cada classe representa uma tabela quando se usa table=True. Os atributos viram colunas, e o tipo (str, int, etc.) ajuda tanto no banco quanto na validação. A função Field configura detalhes, como chave primária, valor padrão e nulabilidade. Isso é parecido com “desenhar” o formato da tabela em Python. E como o modelo também é um schema Pydantic, ele valida dados de entrada e organiza a saída.
O campo id costuma ser inteiro e chave primária, então ele normalmente começa como None e o banco preenche. Campos como email e name viram colunas de texto. Um detalhe importante é que guardar password como texto puro é inseguro em sistemas reais, então normalmente se guarda um hash, mas o exemplo abaixo mantém simples porque o foco é conexão e fluxo. O atributo __tablename__ fixa o nome da tabela, evitando surpresas quando o nome da classe muda. A seguir vai um modelo de User bem direto.
Este exemplo mostra um modelo SQLModel que vira a tabela users no Postgres.
from sqlmodel import Field, SQLModel
class User(SQLModel, table=True):
__tablename__ = "users"
id: int | None = Field(default=None, primary_key=True)
name: str
email: str
password: str
Engine e Session: a conexão e a “sessão de trabalho” com o banco
Pra conversar com o Postgres, o SQLModel usa um engine criado com create_engine(). Esse engine é criado uma vez e reutilizado, porque abrir conexão do zero o tempo todo é caro. A partir dele, cada requisição pode usar uma Session, que encapsula transações, commits e queries. No FastAPI, o padrão mais limpo é criar uma dependência que “entrega” uma Session pronta. Assim, cada rota recebe uma session e não precisa saber como ela nasceu.
O termo dependência no FastAPI é uma função que o framework executa e injeta o resultado onde for necessário. Quando essa dependência usa yield, ela cria e finaliza recursos no ciclo certo, como abrir e fechar sessão. O tipo Annotated ajuda a definir um “atalho” de tipagem com Depends, ficando fácil reaproveitar. Isso evita repetição nas rotas e dá um padrão único pro app inteiro. A seguir vai um db.py completo com engine, get_session e SessionDep.
Este exemplo mostra um db.py com engine e uma dependência de sessão para injetar no FastAPI.
from typing import Annotated
from fastapi import Depends
from sqlmodel import Session, create_engine
from config import get_settings
engine = create_engine(get_settings().PG_CONNECTION_STRING)
def get_session():
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
Criando tabelas no startup (quando faz sentido e quais são os limites)
Às vezes eu gosto de criar as tabelas automaticamente quando o app inicia, principalmente em projetos pequenos ou protótipos. Isso é feito com SQLModel.metadata.create_all(engine), que cria o que não existe. O detalhe é que isso não é um sistema de migração: se um campo muda, o banco não “se ajusta sozinho”. Ou seja, ele cria tabelas novas, mas não resolve alterações de colunas de forma completa. Em projetos maiores, isso normalmente vira tarefa de ferramentas de migração, mas aqui a ideia é manter simples.
Pra create_all encontrar os modelos, eles precisam estar importados no contexto onde create_all roda. Por isso, eu importo os modelos dentro do db.py (ou em algum lugar central). Também existe um detalhe prático: ferramentas de “organizar imports” podem remover imports “não usados”, então às vezes é preciso garantir que o import continue existindo. Um jeito simples é referenciar a classe numa variável, sem afetar o comportamento. A seguir vai um db.py com create_db_and_tables e o ajuste para manter o import.
Este exemplo mostra um db.py com função para criar tabelas, garantindo que os modelos estejam importados.
from typing import Annotated
from fastapi import Depends
from sqlmodel import SQLModel, Session, create_engine
from config import get_settings
from models.user import User
engine = create_engine(get_settings().PG_CONNECTION_STRING)
def create_db_and_tables():
SQLModel.metadata.create_all(engine)
def get_session():
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_session)]
if __name__ == "__main__":
# Mantém o import do modelo para o create_all enxergar a tabela
modelo_referencia = User
print(modelo_referencia)
Este exemplo mostra um main.py usando lifespan para rodar create_db_and_tables no início do app.
from contextlib import asynccontextmanager
from fastapi import FastAPI
from db import create_db_and_tables
@asynccontextmanager
async def on_startup(app: FastAPI):
create_db_and_tables()
yield
app = FastAPI(lifespan=on_startup)
Usando SessionDep nas rotas: criar, commitar e retornar dados
Com SessionDep pronto, as rotas ficam bem diretas. Inserir um registro geralmente segue a sequência: add para adicionar na sessão, commit para confirmar no banco e refresh para atualizar o objeto com dados gerados (como id). Esse refresh é útil porque o Postgres pode criar valores automaticamente. No FastAPI, a session entra como parâmetro e o framework injeta automaticamente. Isso evita código repetido e reduz chance de esquecer de fechar sessão.
Uma rota de criação normalmente recebe um modelo no body e retorna o mesmo modelo. Em apps reais, costuma existir diferença entre “modelo de entrada” e “modelo de saída”, mas o exemplo funciona para ensinar o fluxo. O retorno em formato JSON é feito automaticamente pelo FastAPI, usando a parte Pydantic do SQLModel. A seguir vai uma rota simples de POST para criar um usuário usando SessionDep.
Este exemplo mostra uma rota que cria User no Postgres usando a sessão injetada.
from fastapi import FastAPI
from db import SessionDep
from models.user import User
app = FastAPI()
@app.post("/user", response_model=User)
def create_user(user: User, session: SessionDep) -> User:
session.add(user)
session.commit()
session.refresh(user)
return user
Separando regras de negócio com um service (organização mais limpa)
Eu costumo separar “rota” de “regra de negócio” usando uma classe de serviço, tipo UsersService. A rota fica responsável por HTTP e validação, enquanto o serviço concentra consultas e mudanças no banco. Isso deixa o código mais legível e evita duplicação quando várias rotas fazem coisas parecidas. Também facilita reaproveitar lógica em outros pontos, como tarefas internas. No dia a dia, essa separação reduz bagunça quando o projeto cresce.
No SQLModel, consultas normalmente usam select() e session.exec(). O select cria a consulta, e o exec roda no banco e devolve um resultado. Para pegar um único registro, métodos como one() ou first() ajudam, mas cada um tem comportamento diferente quando não acha nada. Aqui o foco é demonstrar o padrão, então fica um get_user_by_id simples. A seguir vão um service e depois um router usando Depends para injetar o serviço.
Este exemplo mostra um UsersService que recebe a sessão e centraliza operações de banco.
from sqlmodel import select
from db import SessionDep
from models.user import User
class UsersService:
def __init__(self, db: SessionDep):
self.db = db
def create_user(self, user: User) -> User:
self.db.add(user)
self.db.commit()
self.db.refresh(user)
return user
def get_user_by_id(self, id: int) -> User:
res = self.db.exec(select(User).where(User.id == id))
return res.one()
Este exemplo mostra um router usando Depends para criar o service e chamar a regra de negócio.
from fastapi import APIRouter, Depends
from models.user import User
from users_service import UsersService
router = APIRouter()
@router.post("/user", response_model=User)
def create_user(user: User, service: UsersService = Depends(UsersService)) -> User:
return service.create_user(user)
Fechamento: o fluxo completo FastAPI + Postgres de ponta a ponta
O fluxo que funciona bem, pra mim, começa com variáveis no .env, passa por um Settings central e chega no engine do SQLModel. Em seguida, uma dependência cria uma Session por requisição e injeta em rotas ou serviços. Os modelos SQLModel representam tabelas e também validam o formato dos dados que entram e saem. Quando faz sentido, a criação automática de tabelas no startup acelera o início do projeto.
No final, a aplicação fica com uma base organizada: configuração separada, conexão centralizada e rotas limpas. O Postgres entra como a camada persistente e o FastAPI controla o tráfego HTTP com validação bem automática. Esse padrão segura bem desde um projeto pequeno até um projeto médio, mantendo o código fácil de entender. Com isso, a integração entre FastAPI e Postgres fica direta, repetível e sem mistério.