API RESTful: FAST API

Sitio: Campus virtual DAW - damiansu
Curso: Programación en Python
Libro: API RESTful: FAST API
Imprimido por: Invitado
Día: domingo, 8 de marzo de 2026, 09:04

1. Explicación

.

2. FastAPI básico + Swagger

Proyecto: fastapi-demo

Abrimos el proyecto y activamos el entorno virtual

python -m venv venv

Windows

 .\venv\Scripts\Activate.ps1

Mac

source venv/bin/activate

Instalamos dependencias

pip install fastapi uvicorn

2.1. main.py

from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
    title="Demo FastAPI (sin BD)",
    description="Primero Swagger, luego MySQL",
    version="1.0.0"
)

class ProductoCreate(BaseModel):
    nombre: str = Field(min_length=1, max_length=100)
    precio: float = Field(gt=0)
    stock: int = Field(ge=0, default=0)

@app.get("/")
def root():
    return {"ok": True, "mensaje": "FastAPI funcionando"}

@app.post("/productos")
def crear_producto(producto: ProductoCreate):
    # Simulación: devolvemos lo recibido + un id fijo
    return {"id": 1, **producto.model_dump()}

2.2. Arrancamos el server

uvicorn main:app --reload

Verificamos:

  • http://localhost:8000

  • http://localhost:8000/docs

Lanzamos un POST a /productos

Provocamos un 200 y un 422

3. Conectar con MySQL

.

3.1. Levantamos el servicio

Levantamos el servicio de MySQL

  • puerto: 3307
  • usuario: root
  • contraseña: ChuckNorris2025

Los datos a importar son estos;

CREATE DATABASE IF NOT EXISTS fastapi_db
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

USE fastapi_db;

DROP TABLE IF EXISTS productos;

CREATE TABLE productos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nombre VARCHAR(100) NOT NULL,
    precio DECIMAL(10,2) NOT NULL,
    stock INT NOT NULL DEFAULT 0
);

INSERT INTO productos (nombre, precio, stock) VALUES
('Portátil Lenovo', 899.99, 10),
('Monitor LG 27', 249.50, 15),
('Teclado Mecánico', 79.99, 30);

3.2. db.py

Instalamos dependencias

pip install sqlalchemy pymysql cryptography

Creamos db.py, cambiar valores:

  • puerto: 3307
  • usuario: root
  • contraseña: ChuckNorris2025
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base

DATABASE_URL = "mysql+pymysql://root:ChuckNorris2025@localhost:3307/fastapi_db"

engine = create_engine(DATABASE_URL, echo=True)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

3.3. models.py

Creamos los modelos de las tablas, models.py

from sqlalchemy import Column, Integer, String, Numeric
from db import Base

class Producto(Base):
    __tablename__ = "productos"

    id = Column(Integer, primary_key=True, index=True)
    nombre = Column(String(100), nullable=False)
    precio = Column(Numeric(10, 2), nullable=False)
    stock = Column(Integer, nullable=False, default=0)

3.4. main.py (modificación)

Modificamos main.py

from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session

from db import get_db
from models import Producto

app = FastAPI(
    title="FastAPI + Swagger + MySQL",
    description="Sesión práctica: primero Swagger y luego persistencia real",
    version="2.0.0"
)

class ProductoCreate(BaseModel):
    nombre: str = Field(min_length=1, max_length=100)
    precio: float = Field(gt=0)
    stock: int = Field(ge=0, default=0)

class ProductoResponse(ProductoCreate):
    id: int
    class Config:
        from_attributes = True  # Pydantic v2

@app.get("/")
def root():
    return {"ok": True, "mensaje": "API con MySQL lista. Ve a /docs"}

@app.get("/productos", response_model=list[ProductoResponse])
def listar_productos(db: Session = Depends(get_db)):
    return db.query(Producto).all()

@app.get("/productos/{producto_id}", response_model=ProductoResponse)
def obtener_producto(producto_id: int, db: Session = Depends(get_db)):
    prod = db.query(Producto).filter(Producto.id == producto_id).first()
    if not prod:
        raise HTTPException(status_code=404, detail="Producto no encontrado")
    return prod

@app.post("/productos", response_model=ProductoResponse, status_code=201)
def crear_producto(producto: ProductoCreate, db: Session = Depends(get_db)):
    nuevo = Producto(nombre=producto.nombre, precio=producto.precio, stock=producto.stock)
    db.add(nuevo)
    db.commit()
    db.refresh(nuevo)
    return nuevo

3.5. Verificamos

Verificamos los métodos y comprobamos que el POST cambia el estado del  base de datos:

Primeras conclusiones

  • Primero: API y documentación (Swagger)

  • Segundo: persistencia real (MySQL + ORM)

  • FastAPI no crea la BD: se integra con el ORM (como JPA en Java)

4. JWT BÁSICO EN FASTAPI

No metemos bases de datos en la autenciación

Partimos de:

  • FastAPI funcionando

  • Swagger visible (/docs)

  • Endpoint simple ya probado

4.1. Instalar dependencias JWT

Dependencias

  • bcrypt → contraseñas seguras

  • jose → firmar/verificar JWT

  • multipart → formulario de login en Swagger

pip install python-jose passlib[bcrypt<5] python-multipart

si no tira probad esta: 

pip install python-jose passlib[bcrypt] python-multipart bcrypt<5

para mac

pip install python-jose "passlib[bcrypt]" python-multipart "bcrypt<5"

4.2. Crear auth.py

from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from security import hash_password, verify_password, create_token

router = APIRouter(tags=["auth"])

# Usuario fijo SOLO para clase (sin BD)
FAKE_USER = {
    "username": "admin",
    "password_hash": hash_password("ChuckNorris2026")
}

@router.post("/login")
def login(form: OAuth2PasswordRequestForm = Depends()):
    if form.username != FAKE_USER["username"]:
        raise HTTPException(status_code=401, detail="Usuario incorrecto")

    if not verify_password(form.password, FAKE_USER["password_hash"]):
        raise HTTPException(status_code=401, detail="Password incorrecto")

    token = create_token(form.username)
    return {"access_token": token, "token_type": "bearer"}

4.3. Crear security.py

from jose import jwt
from passlib.context import CryptContext

SECRET_KEY = "ChuckNorris2026"
ALGORITHM = "HS256"

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(password: str, password_hash: str) -> bool:
    return pwd_context.verify(password, password_hash)

def create_token(username: str) -> str:
    payload = {"sub": username}
    return jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)

def decode_token(token: str) -> str:
    payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
    return payload["sub"]

4.4. Crear deps.py

from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError
from security import decode_token

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

def get_current_user(token: str = Depends(oauth2_scheme)) -> str:
    try:
        return decode_token(token)
    except JWTError:
        raise HTTPException(status_code=401, detail="Token inválido")

4.5. Modificar main.py

from fastapi import FastAPI, Depends
from auth import router as auth_router
from deps import get_current_user

app = FastAPI(
    title="FastAPI + Swagger + JWT (básico)",
    description="Login → token → endpoint protegido",
    version="1.0.0"
)

app.include_router(auth_router)

@app.get("/")
def root():
    return {"ok": True, "mensaje": "Ve a /docs para probar JWT"}

@app.get("/privado")
def privado(usuario: str = Depends(get_current_user)):
    return {"mensaje": f"Hola {usuario}, estás autenticado"}

from fastapi import FastAPI, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from auth import router as auth_router
from deps import get_current_user

from db import get_db
from models import Producto

app = FastAPI(
    title="FastAPI + Swagger + JWT (básico)",
    description="Login → token → endpoint protegido",
    version="1.0.0"
)

class ProductoCreate(BaseModel):
    nombre: str = Field(min_length=1, max_length=100)
    precio: float = Field(gt=0)
    stock: int = Field(ge=0, default=0)

class ProductoResponse(ProductoCreate):
    id: int
    class Config:
        from_attributes = True  # Pydantic v2

app.include_router(auth_router)

@app.get("/")
def root():
    return {"ok": True, "mensaje": "API con MySQL lista. Ve a /docs"}

@app.get("/privado")
def privado(usuario: str = Depends(get_current_user)):
    return {"mensaje": f"Hola {usuario}, estás autenticado"}


@app.get("/productos", response_model=list[ProductoResponse])
def listar_productos(db: Session = Depends(get_db)):
    return db.query(Producto).all()

@app.get("/productos/{producto_id}", response_model=ProductoResponse)
def obtener_producto(producto_id: int, db: Session = Depends(get_db)):
    prod = db.query(Producto).filter(Producto.id == producto_id).first()
    if not prod:
        raise HTTPException(status_code=404, detail="Producto no encontrado")
    return prod

@app.post("/productos", response_model=ProductoResponse, status_code=201)
def crear_producto(producto: ProductoCreate, db: Session = Depends(get_db)):
    nuevo = Producto(nombre=producto.nombre, precio=producto.precio, stock=producto.stock)
    db.add(nuevo)
    db.commit()
    db.refresh(nuevo)
    return nuevo

4.6. Verificamos con Swagger

4.7. Añadimos un endpoint con JWT

Explicación

/nombre -> el nombre lo saca del token

@app.get("/nombre")
def nombre(usuario: str = Depends(get_current_user)):
    return{"usuario": usuario}

4.8. Protegemos el POST

Explicación

@app.post("/productos", response_model=ProductoResponse, status_code=201)
def crear_producto(
    producto: ProductoCreate,
    db: Session = Depends(get_db),
    usuario: str = Depends(get_current_user)
):
    nuevo = Producto(
        nombre=producto.nombre,
        precio=producto.precio,
        stock=producto.stock
    )
    db.add(nuevo)
    db.commit()
    db.refresh(nuevo)
    return nuevo

4.9. PUT /productos/{producto_id} (PROTEGIDO)

Explicación

@app.put("/productos/{producto_id}", response_model=ProductoResponse)
def actualizar_producto(
    producto_id: int,
    producto: ProductoCreate,
    db: Session = Depends(get_db),
    usuario: str = Depends(get_current_user)
):
    existente = db.query(Producto).filter(Producto.id == producto_id).first()
    if not existente:
        raise HTTPException(status_code=404, detail="Producto no encontrado")

    existente.nombre = producto.nombre
    existente.precio = producto.precio
    existente.stock = producto.stock

    db.commit()
    db.refresh(existente)
    return existente

4.10. DELETE /productos/{producto_id} (PROTEGIDO)

@app.delete("/productos/{producto_id}", status_code=204)
def eliminar_producto(
    producto_id: int,
    db: Session = Depends(get_db),
    usuario: str = Depends(get_current_user)
):
    existente = db.query(Producto).filter(Producto.id == producto_id).first()
    if not existente:
        raise HTTPException(status_code=404, detail="Producto no encontrado")

    db.delete(existente)
    db.commit()
    return