Saltar al contenido
CodigoDev

Cómo construir una API rápida con FastAPI y Python

folder Backend Tutorial Inicial
Ilustración de una API REST rápida construida con FastAPI y Python
info Introducción a la guía

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.

task_alt

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

Python 3.12, pip, conocimientos básicos de HTTP, JSON, manejo de la terminal y fundamentos de Python.
Guía paso a paso verificada CodigoDev

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.

  • pip y venv.

  • 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 venv

Activa el entorno en macOS o Linux:

source venv/bin/activate

En Windows PowerShell:

.\venv\Scripts\Activate.ps1

Organiza 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.txt

Crea 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.0
pip install -r requirements.txt

5. 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 --reload

app.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 == 422

Ejecuta 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 .env al repositorio.

  • Utiliza Pydantic v2 y response_model.

  • Aplica paginación a los listados.

  • Centraliza los manejadores de errores en aplicaciones grandes.

  • No uses --reload en producción.

  • Usa un servidor ASGI preparado para múltiples workers.

  • Utiliza Alembic en lugar de create_all para proyectos reales.

  • Configura CORSMiddleware si 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.

C
Publicado por Equipo CodigoDev
calendar_today
Stack de la guía

Herramientas y su función en este proceso

No es una lista genérica: cada recurso tiene un papel concreto dentro del proceso.

Logo de FastAPI, framework backend para APIs con Python

Framework principal

FastAPI

Principal

FastAPI se utiliza para crear las rutas, validar las peticiones, generar la documentación interactiva y construir la API REST con Python.

Sigue mejorando tus proyectos con otros flujos prácticos.

Ver todas las guías arrow_forward