Cómo construir una API rápida con FastAPI y Python
Aprende paso a paso a crear una API REST rápida y moderna con FastAPI, Python 3.12 y Pydantic v2. Incluye conexión a SQLite con SQLAlchemy y buenas prácticas.
Qué conseguirás
Al finalizar, tendrás una API REST completa con operaciones CRUD, validaciones automáticas, base de datos y pruebas automatizadas.
groups Público objetivo
Desarrolladores que inician con Python y buscan un framework moderno y rápido para construir APIs RESTful profesionales.
checklist Antes de empezar / Requisitos previos
1. Introducción
FastAPI se ha consolidado como uno de los frameworks web modernos y rápidos para construir APIs con Python. Basado en el tipado de Python, valida automáticamente los datos, reduce errores y genera documentación interactiva con Swagger y ReDoc.
En este tutorial aprenderás paso a paso a construir una API REST desde cero con FastAPI, Python 3.12, Pydantic v2 y SQLAlchemy 2.x.
2. Qué construiremos
Desarrollaremos una API REST para gestionar productos con estos endpoints:
GET /api/v1/products: listar con paginación y búsqueda.POST /api/v1/products: crear un producto.GET /api/v1/products/{product_id}: consultar el detalle.PUT /api/v1/products/{product_id}: actualizar completamente.PATCH /api/v1/products/{product_id}: actualizar parcialmente.DELETE /api/v1/products/{product_id}: eliminar.
La aplicación utilizará validación automática con Pydantic v2, SQLite mediante SQLAlchemy 2.x, documentación autogenerada y pruebas automatizadas.
3. Requisitos previos
Python 3.12 o superior.
pipyvenv.Una terminal Bash, Zsh o PowerShell.
VS Code, PyCharm u otro editor.
Conocimientos básicos de Python, HTTP y JSON.
4. Crear el proyecto
Crea la carpeta y el entorno virtual:
mkdir fastapi-productos
cd fastapi-productos
python3 -m venv venvActiva el entorno en macOS o Linux:
source venv/bin/activateEn Windows PowerShell:
.\venv\Scripts\Activate.ps1Organiza el proyecto con esta estructura:
fastapi-productos/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ ├── schemas.py
│ ├── crud.py
│ └── routers/
│ ├── __init__.py
│ └── products.py
├── tests/
│ ├── __init__.py
│ └── test_products.py
└── requirements.txtCrea las dependencias:
# requirements.txt
fastapi>=0.110.0
uvicorn>=0.29.0
sqlalchemy>=2.0.29
pydantic>=2.6.4
pytest>=8.1.1
httpx>=0.27.0pip install -r requirements.txt5. Configurar FastAPI y Uvicorn
Crea el punto de entrada de la aplicación:
# app/main.py
from fastapi import FastAPI
app = FastAPI(
title='API de Productos',
description='Una API REST rápida construida con FastAPI y Python',
version='1.0.0',
)
@app.get('/health', tags=['Health'])
def health_check() -> dict[str, str]:
return {'status': 'ok', 'message': 'API funcionando correctamente'}Ejecuta el servidor:
uvicorn app.main:app --reloadapp.main:app indica el módulo y el objeto de aplicación. Usa --reload únicamente durante el desarrollo.
6. Configurar SQLite y SQLAlchemy
SQLite permite trabajar localmente sin instalar un servidor de base de datos.
# app/database.py
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker
SQLALCHEMY_DATABASE_URL = 'sqlite:///./productos.db'
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
connect_args={'check_same_thread': False},
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
class Base(DeclarativeBase):
pass
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()La dependencia get_db crea una sesión por petición y la cierra incluso cuando ocurre un error.
7. Crear el modelo Product
# app/models.py
from datetime import datetime
from sqlalchemy import Boolean, DateTime, Float, Integer, String, func
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class Product(Base):
__tablename__ = 'products'
id: Mapped[int] = mapped_column(primary_key=True, index=True)
name: Mapped[str] = mapped_column(String(255), index=True, nullable=False)
description: Mapped[str | None] = mapped_column(String, nullable=True)
price: Mapped[float] = mapped_column(Float, nullable=False)
stock: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), nullable=False)8. Crear los esquemas Pydantic
No utilices directamente el modelo de base de datos como esquema de entrada y salida. Separar ambos conceptos permite validar reglas de negocio y controlar los campos expuestos.
# app/schemas.py
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field
class ProductBase(BaseModel):
name: str = Field(min_length=2, max_length=255, description='Nombre del producto')
description: str | None = None
price: float = Field(ge=0, description='Precio mayor o igual a cero')
stock: int = Field(default=0, ge=0, description='Stock no negativo')
is_active: bool = True
class ProductCreate(ProductBase):
pass
class ProductUpdate(ProductBase):
pass
class ProductPatch(BaseModel):
name: str | None = Field(default=None, min_length=2, max_length=255)
description: str | None = None
price: float | None = Field(default=None, ge=0)
stock: int | None = Field(default=None, ge=0)
is_active: bool | None = None
class ProductResponse(ProductBase):
id: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)9. Crear la capa CRUD
# app/crud.py
from sqlalchemy import select
from sqlalchemy.orm import Session
from app import models, schemas
def get_product(db: Session, product_id: int) -> models.Product | None:
return db.scalar(select(models.Product).where(models.Product.id == product_id))
def get_products(
db: Session,
skip: int = 0,
limit: int = 100,
search: str | None = None,
) -> list[models.Product]:
query = select(models.Product).offset(skip).limit(limit)
if search:
query = query.where(models.Product.name.ilike(f'%{search}%'))
return list(db.scalars(query).all())
def create_product(db: Session, product: schemas.ProductCreate) -> models.Product:
db_product = models.Product(**product.model_dump())
db.add(db_product)
db.commit()
db.refresh(db_product)
return db_product
def update_product(
db: Session,
db_product: models.Product,
product_update: schemas.ProductUpdate | schemas.ProductPatch,
) -> models.Product:
for key, value in product_update.model_dump(exclude_unset=True).items():
setattr(db_product, key, value)
db.commit()
db.refresh(db_product)
return db_product
def delete_product(db: Session, db_product: models.Product) -> None:
db.delete(db_product)
db.commit()10. Crear el router de productos
# app/routers/products.py
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from app import crud, schemas
from app.database import get_db
router = APIRouter(prefix='/api/v1/products', tags=['Products'])
@router.get('', response_model=list[schemas.ProductResponse])
def read_products(
skip: int = 0,
limit: int = 10,
search: str | None = None,
db: Session = Depends(get_db),
) -> list[schemas.ProductResponse]:
return crud.get_products(db, skip=skip, limit=limit, search=search)
@router.post('', response_model=schemas.ProductResponse, status_code=status.HTTP_201_CREATED)
def create_product(product: schemas.ProductCreate, db: Session = Depends(get_db)) -> schemas.ProductResponse:
return crud.create_product(db=db, product=product)
@router.get('/{product_id}', response_model=schemas.ProductResponse)
def read_product(product_id: int, db: Session = Depends(get_db)) -> schemas.ProductResponse:
db_product = crud.get_product(db, product_id=product_id)
if db_product is None:
raise HTTPException(status_code=404, detail='Producto no encontrado')
return db_product
@router.put('/{product_id}', response_model=schemas.ProductResponse)
def update_product_full(
product_id: int,
product: schemas.ProductUpdate,
db: Session = Depends(get_db),
) -> schemas.ProductResponse:
db_product = crud.get_product(db, product_id=product_id)
if db_product is None:
raise HTTPException(status_code=404, detail='Producto no encontrado')
return crud.update_product(db=db, db_product=db_product, product_update=product)
@router.patch('/{product_id}', response_model=schemas.ProductResponse)
def update_product_partial(
product_id: int,
product: schemas.ProductPatch,
db: Session = Depends(get_db),
) -> schemas.ProductResponse:
db_product = crud.get_product(db, product_id=product_id)
if db_product is None:
raise HTTPException(status_code=404, detail='Producto no encontrado')
return crud.update_product(db=db, db_product=db_product, product_update=product)
@router.delete('/{product_id}', status_code=status.HTTP_204_NO_CONTENT)
def delete_product(product_id: int, db: Session = Depends(get_db)) -> None:
db_product = crud.get_product(db, product_id=product_id)
if db_product is None:
raise HTTPException(status_code=404, detail='Producto no encontrado')
crud.delete_product(db, db_product)Conecta el router e inicializa las tablas:
# app/main.py
from fastapi import FastAPI
from app.database import Base, engine
from app.routers import products
Base.metadata.create_all(bind=engine)
app = FastAPI(
title='API de Productos',
description='Una API REST rápida construida con FastAPI y Python',
version='1.0.0',
)
app.include_router(products.router)
@app.get('/health', tags=['Health'])
def health_check() -> dict[str, str]:
return {'status': 'ok', 'message': 'API funcionando correctamente'}Base.metadata.create_all es suficiente para este ejemplo. En producción debes utilizar migraciones con Alembic.
11. Probar la API con Swagger UI
Inicia el servidor y visita:
http://localhost:8000/docs: Swagger UI interactivo.http://localhost:8000/redoc: documentación alternativa con ReDoc.
En Swagger puedes abrir un endpoint, pulsar Try it out, introducir el JSON y ejecutar la petición.
12. Probar con cURL
Crear un producto
curl -X POST 'http://localhost:8000/api/v1/products' \
-H 'Content-Type: application/json' \
-d '{"name":"Teclado Mecánico","description":"Switches red","price":89.99,"stock":50,"is_active":true}'Listar productos
curl -X GET 'http://localhost:8000/api/v1/products?skip=0&limit=10'Actualizar parcialmente
curl -X PATCH 'http://localhost:8000/api/v1/products/1' \
-H 'Content-Type: application/json' \
-d '{"price":79.99}'Actualizar completamente
curl -X PUT 'http://localhost:8000/api/v1/products/1' \
-H 'Content-Type: application/json' \
-d '{"name":"Teclado Mecánico V2","description":"Nueva versión","price":99.99,"stock":30,"is_active":true}'Eliminar
curl -X DELETE 'http://localhost:8000/api/v1/products/1'13. Respuestas JSON
Un producto creado se devuelve según el esquema ProductResponse:
{
"name": "Teclado Mecánico",
"description": "Switches red",
"price": 89.99,
"stock": 50,
"is_active": true,
"id": 1,
"created_at": "2026-10-24T14:35:22.123456"
}Una lista devuelve un arreglo de productos. Un error de validación 422 tiene esta forma:
{
"detail": [
{
"type": "greater_than_equal",
"loc": ["body", "price"],
"msg": "Input should be greater than or equal to 0",
"input": -15.5
}
]
}Un producto inexistente devuelve:
{"detail":"Producto no encontrado"}14. Códigos HTTP
200 OK: lectura y actualización exitosas.
201 Created: producto creado.
204 No Content: eliminación exitosa sin cuerpo.
404 Not Found: producto inexistente.
422 Unprocessable Entity: datos que no cumplen la validación.
500 Internal Server Error: fallo no controlado del servidor.
15. Pruebas automatizadas
Usaremos pytest y TestClient, basado en HTTPX:
# tests/test_products.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health_check() -> None:
response = client.get('/health')
assert response.status_code == 200
assert response.json()['status'] == 'ok'
def test_create_product() -> None:
response = client.post('/api/v1/products', json={
'name': 'Monitor 4K',
'description': 'Monitor para programar',
'price': 300.50,
'stock': 10,
})
assert response.status_code == 201
assert response.json()['name'] == 'Monitor 4K'
def test_read_products() -> None:
response = client.get('/api/v1/products')
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_get_non_existent_product() -> None:
response = client.get('/api/v1/products/999')
assert response.status_code == 404
def test_validation_error_negative_price() -> None:
response = client.post('/api/v1/products', json={
'name': 'Ratón', 'price': -10, 'stock': 5,
})
assert response.status_code == 422
def test_validation_error_missing_name() -> None:
response = client.post('/api/v1/products', json={'price': 10, 'stock': 5})
assert response.status_code == 422Ejecuta las pruebas con:
pytest tests/En un proyecto real, configura una base de datos de pruebas aislada para que cada prueba sea independiente.
16. Buenas prácticas
Separa routers, modelos, esquemas y acceso a datos.
Usa variables de entorno para credenciales y secretos.
No subas archivos
.enval repositorio.Utiliza Pydantic v2 y
response_model.Aplica paginación a los listados.
Centraliza los manejadores de errores en aplicaciones grandes.
No uses
--reloaden producción.Usa un servidor ASGI preparado para múltiples workers.
Utiliza Alembic en lugar de
create_allpara proyectos reales.Configura
CORSMiddlewaresi un frontend externo consumirá la API.
17. Errores frecuentes
No activar el entorno virtual
Si aparecen errores de importación, activa venv antes de instalar o ejecutar dependencias.
Error al importar módulos
Ejecuta Uvicorn desde la carpeta raíz donde vive app.
No encontrar la base de datos
La ruta relativa ./productos.db depende del directorio desde el que ejecutas Uvicorn.
Mezclar Pydantic v1 y v2
En Pydantic v2 utiliza model_dump(), ConfigDict y model_config en lugar de APIs antiguas.
Error 422 persistente
Revisa que los tipos y nombres del JSON coincidan con el esquema Pydantic.
No cerrar la sesión
La dependencia con yield db y finally: db.close() libera la conexión después de cada petición.
18. Conclusión
Has construido una API REST moderna con FastAPI, Pydantic v2 y SQLAlchemy. Aprendiste a validar datos, separar el acceso a la base de datos de los routers, utilizar inyección de dependencias y verificar el funcionamiento con pruebas automatizadas. La combinación de tipado, documentación automática y rendimiento convierte a FastAPI en una opción sólida para construir APIs profesionales.
19. Próximos pasos
Una vez dominado este CRUD, puedes añadir migraciones con Alembic, PostgreSQL, autenticación JWT, Docker, rate limiting, caching y una arquitectura de configuración por entornos.
Herramientas y su función en este proceso
No es una lista genérica: cada recurso tiene un papel concreto dentro del proceso.
Framework principal
FastAPI
FastAPI se utiliza para crear las rutas, validar las peticiones, generar la documentación interactiva y construir la API REST con Python.
Continúa con una guía relacionada
Sigue mejorando tus proyectos con otros flujos prácticos.