Introducción al Desarrollo de APIs
Construye servicios web modernos con FastAPI, Flask y las mejores prácticas REST
Las APIs (Application Programming Interfaces) son el corazón de la arquitectura de software moderna. Permiten que diferentes aplicaciones se comuniquen entre sí, intercambien datos y expongan funcionalidades de manera estandarizada. En este capítulo exploraremos cómo construir APIs robustas en Python usando frameworks como FastAPI y Flask.
Desde los fundamentos del protocolo HTTP hasta la implementación de operaciones CRUD completas, pasando por validación de datos con Pydantic, documentación automática y manejo de errores, este módulo te proporcionará las herramientas necesarias para crear servicios web profesionales.
Fundamentos de HTTP y REST
HTTP (HyperText Transfer Protocol) es el protocolo fundamental de la web. Cada vez que navegas por internet o consumes una API, estás utilizando HTTP. REST (REpresentational State Transfer) es un estilo arquitectónico que aprovecha HTTP para crear servicios web escalables y mantenibles.
Métodos HTTP Principales
| Método | Operación CRUD | Descripción |
|---|---|---|
| GET | Read | Obtener recursos del servidor |
| POST | Create | Crear nuevos recursos |
| PUT | Update | Actualizar recursos existentes |
| DELETE | Delete | Eliminar recursos |
Arquitectura Cliente-Servidor
Realizando Peticiones con requests
import requests # Petición GET - Obtener datos url = "https://httpbin.org/get" resp = requests.get(url, params={"name": "Python"}) print(resp.json()) # Petición POST - Enviar datos url = "https://httpbin.org/post" data = {"title": "Learn Python", "author": "Fabrizio"} resp = requests.post(url, json=data) print(resp.status_code) # 200 print(resp.json())
200
{'json': {'title': 'Learn Python', 'author': 'Fabrizio'}, ...}
requests simplifica enormemente las peticiones HTTP. El método json() del objeto response decodifica automáticamente la respuesta JSON a un diccionario Python.
FastAPI: Framework Moderno
FastAPI es un framework web moderno y de alto rendimiento para construir APIs con Python. Ofrece validación automática de datos, documentación interactiva, y soporte nativo para operaciones asíncronas.
Características Principales
Tu Primera API con FastAPI
from fastapi import FastAPI # Crear instancia de la aplicación app = FastAPI( title="Mi Primera API", description="API de ejemplo con FastAPI", version="1.0.0" ) @app.get("/") def root(): return {"message": "¡Hola, API!"} @app.get("/items/{item_id}") def read_item(item_id: int, q: str | None = None): return {"item_id": item_id, "query": q}
INFO: Application startup complete.
uvicorn main:app --reload para iniciar el servidor. La documentación interactiva estará disponible en /docs (Swagger) y /redoc (ReDoc).
Validación con Pydantic
Pydantic es una librería de validación de datos que utiliza type hints de Python para definir esquemas. FastAPI la integra nativamente para validar automáticamente los datos de entrada y salida.
Definiendo Modelos
from pydantic import BaseModel, Field, EmailStr from typing import Optional from datetime import datetime class UserBase(BaseModel): """Esquema base para usuarios.""" email: EmailStr name: str = Field(..., min_length=1, max_length=100) age: int = Field(..., ge=18, le=120) class UserCreate(UserBase): """Esquema para crear usuarios.""" password: str = Field(..., min_length=8) class UserResponse(UserBase): """Esquema de respuesta (sin password).""" id: int created_at: datetime is_active: bool = True class Config: from_attributes = True # Permite usar objetos ORM
Validación Automática en Endpoints
from fastapi import FastAPI, HTTPException from models import UserCreate, UserResponse app = FastAPI() users_db = {} # Simulación de base de datos @app.post("/users/", response_model=UserResponse) def create_user(user: UserCreate): # Pydantic valida automáticamente los datos user_id = len(users_db) + 1 # Simular guardado en DB user_data = user.model_dump() user_data["id"] = user_id user_data["created_at"] = datetime.now() users_db[user_id] = user_data return user_data @app.get("/users/{user_id}", response_model=UserResponse) def get_user(user_id: int): if user_id not in users_db: raise HTTPException( status_code=404, detail="Usuario no encontrado" ) return users_db[user_id]
Operaciones CRUD Completas
CRUD representa las cuatro operaciones básicas de persistencia: Create, Read, Update y Delete. Implementar estas operaciones de manera correcta es fundamental para cualquier API RESTful.
Implementación Completa
from fastapi import FastAPI, HTTPException, status from pydantic import BaseModel from typing import Optional app = FastAPI() # Modelos class Item(BaseModel): name: str description: Optional[str] = None price: float in_stock: bool = True class ItemUpdate(BaseModel): name: Optional[str] = None description: Optional[str] = None price: Optional[float] = None in_stock: Optional[bool] = None # Base de datos simulada items_db: dict[int, dict] = {} counter = 0 # CREATE @app.post("/items/", status_code=status.HTTP_201_CREATED) def create_item(item: Item): global counter counter += 1 items_db[counter] = {"id": counter, **item.model_dump()} return items_db[counter] # READ (todos) @app.get("/items/") def read_items(skip: int = 0, limit: int = 10): items = list(items_db.values()) return items[skip : skip + limit] # READ (uno) @app.get("/items/{item_id}") def read_item(item_id: int): if item_id not in items_db: raise HTTPException(status_code=404, detail="Item not found") return items_db[item_id] # UPDATE @app.put("/items/{item_id}") def update_item(item_id: int, item: ItemUpdate): if item_id not in items_db: raise HTTPException(status_code=404, detail="Item not found") stored_item = items_db[item_id] update_data = item.model_dump(exclude_unset=True) for key, value in update_data.items(): stored_item[key] = value return stored_item # DELETE @app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_item(item_id: int): if item_id not in items_db: raise HTTPException(status_code=404, detail="Item not found") del items_db[item_id] return None
exclude_unset=True en model_dump() permite actualizaciones parciales, incluyendo solo los campos que el cliente envió explícitamente.
Flask: Framework Minimalista
Flask es un microframework ligero y flexible. Es ideal para APIs pequeñas o cuando necesitas control total sobre la arquitectura de tu aplicación.
API Básica con Flask
from flask import Flask, jsonify, request, abort app = Flask(__name__) # Base de datos en memoria books = [ {"id": 1, "title": "Clean Code", "author": "Robert Martin"}, {"id": 2, "title": "Design Patterns", "author": "Gang of Four"}, ] @app.route("/api/books", methods=["GET"]) def get_books(): return jsonify(books) @app.route("/api/books/<int:book_id>", methods=["GET"]) def get_book(book_id): book = next((b for b in books if b["id"] == book_id), None) if book is None: abort(404) return jsonify(book) @app.route("/api/books", methods=["POST"]) def create_book(): if not request.json: abort(400) new_book = { "id": books[-1]["id"] + 1 if books else 1, "title": request.json.get("title"), "author": request.json.get("author"), } books.append(new_book) return jsonify(new_book), 201 if __name__ == "__main__": app.run(debug=True)
* Debug mode: on
Códigos de Estado HTTP
Los códigos de estado HTTP comunican el resultado de una petición. Usar los códigos correctos hace tu API más intuitiva y fácil de debuggear.
Códigos Más Comunes
| Código | Nombre | Uso |
|---|---|---|
| 200 | OK | Petición exitosa (GET, PUT) |
| 201 | Created | Recurso creado exitosamente (POST) |
| 204 | No Content | Éxito sin contenido de respuesta (DELETE) |
| 400 | Bad Request | Datos de entrada inválidos |
| 401 | Unauthorized | Autenticación requerida |
| 404 | Not Found | Recurso no encontrado |
| 422 | Unprocessable Entity | Error de validación |
| 500 | Internal Server Error | Error del servidor |
Manejo de Excepciones Personalizado
from fastapi import FastAPI, Request from fastapi.responses import JSONResponse app = FastAPI() # Excepción personalizada class ItemNotFoundException(Exception): def __init__(self, item_id: int): self.item_id = item_id # Handler personalizado @app.exception_handler(ItemNotFoundException) async def item_not_found_handler( request: Request, exc: ItemNotFoundException ): return JSONResponse( status_code=404, content={ "error": "not_found", "message": f"Item con ID {exc.item_id} no existe", "item_id": exc.item_id } ) @app.get("/items/{item_id}") def get_item(item_id: int): if item_id > 100: raise ItemNotFoundException(item_id) return {"item_id": item_id}
Dependencias e Inyección
FastAPI tiene un poderoso sistema de inyección de dependencias que permite reutilizar lógica común, gestionar conexiones a bases de datos, autenticación y más.
Sistema de Dependencias
from fastapi import FastAPI, Depends, HTTPException, Header app = FastAPI() # Dependencia: verificar API key async def verify_api_key( x_api_key: str = Header(...) ): if x_api_key != "secret-api-key-123": raise HTTPException( status_code=401, detail="API Key inválida" ) return x_api_key # Dependencia: conexión a BD simulada def get_db(): db = {"connected": True} # Simular conexión try: yield db finally: db["connected"] = False # Cerrar conexión # Endpoint protegido con dependencias @app.get("/secure-data/") async def get_secure_data( api_key: str = Depends(verify_api_key), db: dict = Depends(get_db) ): return { "data": "Información confidencial", "db_status": db["connected"] }
yield permiten ejecutar código de limpieza después de que la respuesta sea enviada, ideal para cerrar conexiones de base de datos.
Variables de Entorno y Configuración
Las credenciales, claves de API y configuraciones sensibles nunca deben estar hardcodeadas. Usa variables de entorno y archivos .env para gestionar la configuración.
Configuración con Pydantic Settings
# Archivo de configuración local DATABASE_URL=postgresql://user:pass@localhost/mydb API_SECRET_KEY=super-secret-key-12345 DEBUG=true ALLOWED_HOSTS=localhost,127.0.0.1
from pydantic_settings import BaseSettings from functools import lru_cache class Settings(BaseSettings): """Configuración de la aplicación.""" database_url: str api_secret_key: str debug: bool = False allowed_hosts: list[str] = ["localhost"] class Config: env_file = ".env" @lru_cache() def get_settings(): return Settings() # Uso en la aplicación from fastapi import FastAPI, Depends app = FastAPI() @app.get("/info") def get_info(settings: Settings = Depends(get_settings)): return { "debug_mode": settings.debug, "hosts": settings.allowed_hosts }
.env en el control de versiones. Añade .env a tu .gitignore y proporciona un archivo .env.example como plantilla.
Rate Limiting y Throttling
Protege tu API del abuso limitando la cantidad de peticiones que un cliente puede realizar en un período de tiempo determinado.
Implementación con Flask-Limiter
from flask import Flask from flask_limiter import Limiter from flask_limiter.util import get_remote_address app = Flask(__name__) # Configurar el limitador limiter = Limiter( get_remote_address, app=app, default_limits=["100 per day", "10 per hour"], storage_uri="memory://", ) # Endpoint con límite por defecto @app.route("/api/standard") def standard_api(): return {"message": "Endpoint estándar"} # Endpoint con límite personalizado @app.route("/api/premium") @limiter.limit("2 per minute") def premium_api(): return {"message": "Endpoint premium (muy limitado)"} # Endpoint sin límite @app.route("/api/health") @limiter.exempt def health_check(): return {"status": "healthy"} if __name__ == "__main__": app.run(debug=True)
{"error": "ratelimit exceeded", "message": "2 per 1 minute"}
Testing de APIs
Las pruebas automatizadas son esenciales para mantener la calidad de tu API. FastAPI incluye un cliente de pruebas integrado basado en httpx.
Tests con Pytest
import pytest from fastapi.testclient import TestClient from main import app # Crear cliente de pruebas client = TestClient(app) def test_read_root(): """Probar endpoint raíz.""" response = client.get("/") assert response.status_code == 200 assert response.json() == {"message": "¡Hola, API!"} def test_create_item(): """Probar creación de item.""" item_data = { "name": "Test Item", "price": 29.99, "in_stock": True } response = client.post("/items/", json=item_data) assert response.status_code == 201 assert response.json()["name"] == "Test Item" def test_item_not_found(): """Probar error 404.""" response = client.get("/items/99999") assert response.status_code == 404 def test_validation_error(): """Probar validación fallida.""" invalid_data = {"name": ""} # Falta price response = client.post("/items/", json=invalid_data) assert response.status_code == 422
test_api.py::test_create_item PASSED
test_api.py::test_item_not_found PASSED
test_api.py::test_validation_error PASSED
======= 4 passed in 0.15s =======
Documentación Automática
FastAPI genera documentación interactiva automáticamente basándose en tus modelos Pydantic y decoradores de ruta.
Enriquecer la Documentación
from fastapi import FastAPI, Query, Path from pydantic import BaseModel, Field app = FastAPI( title="Bookstore API", description="API para gestionar una librería virtual", version="2.0.0", contact={ "name": "Soporte", "email": "api@bookstore.com" } ) class Book(BaseModel): """Modelo de libro con documentación.""" title: str = Field( ..., example="Clean Code", description="Título del libro" ) author: str = Field( ..., example="Robert C. Martin" ) isbn: str = Field( ..., pattern=r"^\d{13}$", example="9780132350884" ) @app.get( "/books/{book_id}", response_model=Book, summary="Obtener libro por ID", description="Retorna los detalles completos de un libro.", responses={ 404: {"description": "Libro no encontrado"} } ) def get_book( book_id: int = Path( ..., ge=1, description="ID único del libro" ) ): """ Busca un libro por su identificador único. - **book_id**: ID numérico del libro (≥ 1) """ return {"title": "Example", "author": "Author", "isbn": "1234567890123"}
/docs para Swagger UI interactivo o /redoc para documentación estilo ReDoc. Ambas se generan automáticamente desde tu código.
Resumen del Capítulo
01 // HTTP y REST
Los métodos GET, POST, PUT y DELETE forman la base de las operaciones CRUD en APIs RESTful.
02 // FastAPI
Framework moderno con validación automática, documentación integrada y alto rendimiento.
03 // Pydantic
Define esquemas de datos con type hints para validación y serialización automática.
04 // Flask
Microframework minimalista ideal para APIs pequeñas o control arquitectónico total.
05 // Códigos HTTP
Usa códigos de estado apropiados: 2xx éxito, 4xx errores del cliente, 5xx errores del servidor.
06 // Seguridad
Implementa autenticación, rate limiting y nunca expongas credenciales en el código.