Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 23 de 30

Introducción a FastAPI

La misma API en cincuenta líneas, diez veces más rápida serializando, y el error de validación que te devuelve la contraseña que acabas de rechazar.

⏱ 16 min de lectura

Empieza el último bloque del curso. FastAPI es la otra forma de escribir una API en Python, y lleva unos años siendo la que todo el mundo recomienda.

Las comparaciones que se leen por ahí son de dos tipos: las que dicen que es diez veces más rápido y las que dicen que eso no importa. Vamos a medirlo, y vamos a ver que las dos tienen parte de razón y ninguna dice lo que de verdad decide la elección.

La misma API#

Esta es entera, con tres puntos de acceso, validación y documentación:

python
from datetime import date
from decimal import Decimal
from enum import Enum

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, Field

app = FastAPI(title='API del catálogo', version='1.0.0')


class Estado(str, Enum):
        borrador = 'borrador'
    publicado = 'publicado'
    descatalogado = 'descatalogado'


class LibroEntrada(BaseModel):
        titulo: str = Field(max_length=200)
    isbn: str = Field(max_length=17)
    precio: Decimal = Field(max_digits=7, decimal_places=2)
    paginas: int = Field(default=0, ge=0)
    estado: Estado = Estado.borrador
    publicado_en: date | None = None


class Libro(LibroEntrada):
        id: int


@app.get('/api/libros/')
def listar(estado: Estado | None = None,
           limite: int = Query(default=20, le=100)) -> list[Libro]:
        libros = [l for l in LIBROS.values() if estado is None or l.estado == estado]
    return libros[:limite]


@app.get('/api/libros/{libro_id}')
def obtener(libro_id: int) -> Libro:
        if libro_id not in LIBROS:
                raise HTTPException(404, 'No existe ese libro.')
    return LIBROS[libro_id]


@app.post('/api/libros/', status_code=201)
def crear(datos: LibroEntrada) -> Libro:
        nuevo = Libro(id=len(LIBROS) + 1, **datos.model_dump())
    LIBROS[nuevo.id] = nuevo
    return nuevo

Cincuenta líneas, sin ficheros de ajustes ni de rutas. Se arranca con:

bash
fastapi dev app/main.py

Y la idea central es esta: las anotaciones de tipo de Python son la especificación. Ese libro_id: int de la ruta hace cuatro cosas a la vez:

Convierte el texto de la dirección a entero.

Valida, devolviendo un error si no es un número.

Documenta, apareciendo en el esquema OpenAPI como entero.

Y ayuda al editor, porque es una anotación normal.

Es la misma idea que la lección 21 pedía a mano con un decorador para arreglar la documentación, aquí llevada al centro del diseño. Nada de esto es un invento de FastAPI: es Python estándar, y por eso no hay que aprender un lenguaje de declaración aparte.

Lo que cuesta instalarlo#

Salida en pantalla
fastapi + uvicorn   14 paquetes,  29 MB
fastapi[standard]   45 paquetes,  74 MB
django + DRF        22 paquetes,  68 MB

Aquí hay una sorpresa. FastAPI tiene fama de ligero, y lo es si lo instalas a secas: catorce paquetes y la mitad de espacio que Django.

Lo que casi todos los tutoriales instalan es fastapi[standard], y eso son cuarenta y cinco paquetes, más que Django con DRF. Y entre ellos:

Salida en pantalla
sentry-sdk 2.70.0
fastapi-cloud-cli 0.26.0
agent-detector 2.0.0
detect-installer 0.2.1

Un cliente de un servicio comercial de nube, un sistema de informes de errores y dos detectores que miran cómo instalaste el paquete. Nada de eso hace falta para servir una API, y en un contenedor de producción son megabytes y superficie de ataque que nadie pidió.

La recomendación práctica: instala lo que necesites y no el ramo entero.

bash
pip install fastapi uvicorn[standard]

Es la misma disciplina que la lección 22 del módulo 2 pedía con las dependencias, y aquí se nota más porque el corchete parece inofensivo.

Lo que cuesta ejecutarlo#

La cifra que se repite es que FastAPI es mucho más rápido. Medido aquí, sirviendo una lista de cien objetos:

Salida en pantalla
FastAPI listado de 100: mediana 0,88 ms
DRF     listado de 100: mediana 7,81 ms

Nueve veces. Y esa comparación es tramposa, porque la de FastAPI no toca la base de datos. La prueba está en el otro caso:

Salida en pantalla
FastAPI un objeto: mediana 0,77 ms
DRF     un objeto: mediana 0,73 ms

DRF va igual o mejor, porque cuando solo hay un objeto la base de datos apenas cuesta y lo que queda es el armazón, que es comparable.

Así que aislemos lo único que de verdad se puede comparar, que es serializar, sin HTTP y sin base de datos, cien objetos con siete campos:

Salida en pantalla
DRF, serializador plano:  0,969 ms
pydantic, validar:        0,098 ms
pydantic, volcar a dict:  0,097 ms
pydantic, volcar a JSON:  0,125 ms

Diez veces, y ese sí es un número honrado. Pydantic tiene el núcleo escrito en Rust y se nota.

Las tres diferencias que sí deciden#

Más importantes que los milisegundos.

Primera: FastAPI no trae base de datos. Ni ORM, ni migraciones, ni panel de administración, ni sistema de usuarios, ni correo. Eso se elige y se monta, normalmente con SQLAlchemy y Alembic, y es la lección 26.

Para un proyecto que necesita todo eso, Django ya lo tiene resuelto y probado, y montarlo pieza a pieza son semanas. Para un servicio que habla con otra cosa y no tiene modelos propios, el armazón de Django es peso muerto.

Segunda: la asincronía es de primera clase. FastAPI la soporta de verdad porque nació así; Django la ha ido añadiendo y el ORM asíncrono aún tiene esquinas. Es la lección 26 y es el argumento técnico real.

Tercera: los errores son distintos, y hay que verlo.

Los errores#

Las dos partes de esto sorprenden. Enviando un cuerpo vacío:

Salida en pantalla
POST /api/libros/  {}
422 Unprocessable Content

{"detail": [
  {"type": "missing", "loc": ["body", "titulo"], "msg": "Field required", "input": {}},
  {"type": "missing", "loc": ["body", "isbn"],   "msg": "Field required", "input": {}},
  {"type": "missing", "loc": ["body", "precio"], "msg": "Field required", "input": {}}
]}

Comparado con DRF:

Salida en pantalla
400 Bad Request
{"titulo": ["Este campo es requerido."],
 "isbn": ["Este campo es requerido."]}

Tres diferencias. El código es 422 y no 400, que es correcto según el estándar y lo usa casi nadie, así que quien consuma tu API tendrá que tratarlo aparte. La forma es una lista de objetos en vez de un diccionario por campo, más informativa y más incómoda de pintar en un formulario. Y están en inglés, sin traducción incluida, al revés que DRF.

Las tres se pueden cambiar con un manejador propio, y conviene hacerlo el primer día, porque cambiar el formato de los errores después es de los cambios que rompen, según la tabla de la lección 22.