Clase 05 — Diseñar tu propia API
De consumidor a creador
Hasta ahora consumimos APIs. Ahora vamos a diseñar y construir una API REST desde cero.
Elegir un framework
| Lenguaje | Framework | Velocidad de desarrollo | Rendimiento |
|---|---|---|---|
| Python | FastAPI | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Python | Flask | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| JavaScript | Express.js | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Go | Gin / Chi | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Rust | Actix / Axum | ⭐⭐ | ⭐⭐⭐⭐⭐ |
Para DevOps, FastAPI (Python) es la elección más popular por su simplicidad y documentación automática.
API mínima con FastAPI
Setup
mkdir pokemon-api && cd pokemon-api
python3 -m venv venv
source venv/bin/activate
pip install fastapi uvicorn
Tu primera API
# main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional
from datetime import datetime
app = FastAPI(title="Pokémon API", version="1.0.0")
class Pokemon(BaseModel):
id: Optional[int] = None
nombre: str
tipo: str
nivel: int = 1
hp: int = 100
class PokemonUpdate(BaseModel):
nombre: Optional[str] = None
tipo: Optional[str] = None
nivel: Optional[int] = None
hp: Optional[int] = None
# "Base de datos" en memoria
db: dict[int, dict] = {
1: {"id": 1, "nombre": "bulbasaur", "tipo": "grass", "nivel": 5, "hp": 45},
25: {"id": 25, "nombre": "pikachu", "tipo": "electric", "nivel": 10, "hp": 35},
}
next_id = 26
@app.get("/health")
def health():
return {"status": "ok", "timestamp": datetime.utcnow().isoformat()}
@app.get("/api/v1/pokemon")
def listar_pokemon(tipo: Optional[str] = None, limit: int = 10, offset: int = 0):
results = list(db.values())
if tipo:
results = [p for p in results if p["tipo"] == tipo]
total = len(results)
results = results[offset:offset + limit]
return {"data": results, "pagination": {"total": total, "limit": limit, "offset": offset}}
@app.get("/api/v1/pokemon/{pokemon_id}")
def obtener_pokemon(pokemon_id: int):
if pokemon_id not in db:
raise HTTPException(status_code=404, detail=f"Pokémon {pokemon_id} no encontrado")
return db[pokemon_id]
@app.post("/api/v1/pokemon", status_code=201)
def crear_pokemon(pokemon: Pokemon):
global next_id
pokemon_dict = pokemon.model_dump()
pokemon_dict["id"] = next_id
db[next_id] = pokemon_dict
next_id += 1
return pokemon_dict
@app.patch("/api/v1/pokemon/{pokemon_id}")
def actualizar_pokemon(pokemon_id: int, updates: PokemonUpdate):
if pokemon_id not in db:
raise HTTPException(status_code=404, detail=f"Pokémon {pokemon_id} no encontrado")
for key, value in updates.model_dump(exclude_none=True).items():
db[pokemon_id][key] = value
return db[pokemon_id]
@app.delete("/api/v1/pokemon/{pokemon_id}", status_code=204)
def eliminar_pokemon(pokemon_id: int):
if pokemon_id not in db:
raise HTTPException(status_code=404, detail=f"Pokémon {pokemon_id} no encontrado")
del db[pokemon_id]
Ejecutar y probar
# Iniciar servidor
uvicorn main:app --reload --port 8000
# En otra terminal:
curl -s http://localhost:8000/health | jq '.'
curl -s http://localhost:8000/api/v1/pokemon | jq '.'
curl -s "http://localhost:8000/api/v1/pokemon?tipo=fire" | jq '.'
curl -s http://localhost:8000/api/v1/pokemon/25 | jq '.'
# Crear
curl -s -X POST http://localhost:8000/api/v1/pokemon \
-H "Content-Type: application/json" \
-d '{"nombre":"eevee","tipo":"normal","nivel":15,"hp":55}' | jq '.'
# Actualizar
curl -s -X PATCH http://localhost:8000/api/v1/pokemon/25 \
-H "Content-Type: application/json" \
-d '{"nivel": 42}' | jq '.'
# Eliminar
curl -s -X DELETE http://localhost:8000/api/v1/pokemon/1 -w "HTTP %{http_code}\n"
# Docs automáticas (FastAPI genera Swagger UI)
# Abrir en navegador: http://localhost:8000/docs
API mínima con Express.js (Node.js)
// server.js
const express = require('express');
const app = express();
app.use(express.json());
let db = {
1: { id: 1, nombre: 'bulbasaur', tipo: 'grass', nivel: 5 },
25: { id: 25, nombre: 'pikachu', tipo: 'electric', nivel: 10 },
};
let nextId = 26;
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
app.get('/api/v1/pokemon', (req, res) => {
let results = Object.values(db);
if (req.query.tipo) results = results.filter(p => p.tipo === req.query.tipo);
res.json({ data: results, total: results.length });
});
app.get('/api/v1/pokemon/:id', (req, res) => {
const pokemon = db[req.params.id];
if (!pokemon) return res.status(404).json({ error: 'No encontrado' });
res.json(pokemon);
});
app.post('/api/v1/pokemon', (req, res) => {
const pokemon = { id: nextId++, ...req.body };
db[pokemon.id] = pokemon;
res.status(201).json(pokemon);
});
app.delete('/api/v1/pokemon/:id', (req, res) => {
if (!db[req.params.id]) return res.status(404).json({ error: 'No encontrado' });
delete db[req.params.id];
res.status(204).send();
});
app.listen(3000, () => console.log('API en http://localhost:3000'));
Dockerizar la API
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# docker-compose.yml
services:
api:
build: .
ports:
- "8000:8000"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
Buenas prácticas de diseño
# 1. Siempre tener /health
GET /health → {"status": "ok"}
# 2. Versionado desde el inicio
/api/v1/pokemon (no /api/pokemon)
# 3. Respuestas consistentes
# Éxito: {"data": [...], "pagination": {...}}
# Error: {"error": {"code": "...", "message": "..."}}
# 4. Códigos HTTP correctos
# 200 GET/PUT/PATCH 201 POST 204 DELETE
# 400 datos inválidos 401 no auth 403 sin permisos 404 no existe
# 5. Paginación por defecto
# Nunca devolver TODOS los registros sin limite
Ejercicios
- Creá una API REST con FastAPI que gestione una lista de tareas (CRUD completo)
- Agregá filtrado por estado (pendiente/completada) y paginación
- Dockerizá la API y probala con curl
- Accedé a
/docsy probá todos los endpoints desde Swagger UI - Escribí un script bash que actúe como cliente de tu API