Cuando necesitas que tu API devuelva una imagen, FastAPI te da varias opciones. La clave es elegir la correcta para no complicarte la vida ni generar un cuello de botella. En mi experiencia, muchos se enredan con esto y terminan usando soluciones más complejas de lo necesario.
Vamos directo al grano con lo más común: tienes los bytes de una imagen en memoria y quieres enviarlos.
Devolviendo una imagen desde memoria con Response
Si ya tienes tu imagen como un objeto bytes —quizás la generaste al vuelo, la leíste de una base de datos o la procesaste—, la forma más directa y eficiente es usar fastapi.responses.Response. Es simple y funciona muy bien.
from fastapi import FastAPI, Response
from io import BytesIO
app = FastAPI()
def generate_cat_picture() -> bytes:
# Esto simula la generación o lectura de una imagen.
# En un caso real, podrías generar una imagen con PIL,
# o leerla de alguna parte.
# Por simplicidad, aquí creamos una imagen PNG muy básica.
from PIL import Image
img_buffer = BytesIO()
Image.new('RGB', (100, 50), color = (73, 109, 137)).save(img_buffer, format='PNG')
return img_buffer.getvalue()
@app.get(
"/image",
# Esto es importante para que FastAPI genere bien la doc de OpenAPI.
# Le dice a la UI de Swagger que esperas un tipo de contenido "image/png".
responses={
200: {
"content": {"image/png": {}}
}
},
# También le decimos a FastAPI que la clase de respuesta será Response,
# para evitar que añada "application/json" como un tipo de respuesta adicional.
response_class=Response
)
def get_image():
image_bytes: bytes = generate_cat_picture()
# Aquí es donde le pasas los bytes y el tipo MIME de la imagen.
return Response(content=image_bytes, media_type="image/png")
Con este código, el navegador o cliente que haga la petición a /image recibirá directamente la imagen PNG. El media_type="image/png" es crucial porque le dice al cliente qué tipo de contenido está recibiendo. Si la imagen fuera JPG, pondrías "image/jpeg".
El bloque responses dentro del decorador @app.get es para la documentación de OpenAPI (la interfaz de Swagger UI). No afecta cómo se envía la respuesta, pero hace que tu documentación sea precisa y amigable para quienes consuman tu API. Es un detalle que a veces se pasa por alto, pero que mejora la experiencia del desarrollador.
Sirviendo imágenes desde el disco con FileResponse
Si la imagen ya existe en tu sistema de archivos, fastapi.responses.FileResponse es tu mejor amigo. Es aún más sencillo porque se encarga de todo: leer el archivo, manejar los headers, e incluso optimizaciones como el ETag si tu servidor ASGI lo soporta.
from fastapi import FastAPI
from fastapi.responses import FileResponse
app = FastAPI()
# Asume que tienes un archivo llamado "mi_gato.png" en la misma carpeta
# o en una ruta accesible.
@app.get("/static_image")
def get_static_image():
file_path = "./mi_gato.png" # Asegúrate de que esta ruta exista
return FileResponse(file_path, media_type="image/png")
Este método es perfecto para servir archivos estáticos directamente. Te olvidas de leer bytes manualmente, y FastAPI lo gestiona de forma óptima. Yo lo uso mucho para imágenes de perfil o logos que no cambian.
Ojo con StreamingResponse: El camino a la sobreingeniería
Aquí es donde veo que muchos se equivocan y se meten en problemas. Algunos tutoriales o respuestas en foros sugieren usar fastapi.responses.StreamingResponse para imágenes, y mi opinión es que, en la mayoría de los casos para imágenes, no es lo que quieres usar.
La StreamingResponse está diseñada para cuando no conoces el tamaño total de la respuesta de antemano, o cuando quieres enviar la data en pequeños pedazos (chunks) a medida que se va generando, sin esperar a tenerlo todo listo. Piensa en un stream de video en vivo, o un log muy largo que se genera dinámicamente.
Pero para una imagen que ya está en memoria (como un bytes) o en disco (como un archivo), la StreamingResponse no aporta ningún beneficio real y puede incluso ser perjudicial. Esto me costó entenderlo bien al principio, de hecho.
Mira este patrón que he visto varias veces, y que no deberías replicar:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from io import BytesIO
app = FastAPI()
def generate_cat_picture() -> bytes:
# (Misma función de ejemplo que antes)
from PIL import Image
img_buffer = BytesIO()
Image.new('RGB', (100, 50), color = (73, 109, 137)).save(img_buffer, format='PNG')
return img_buffer.getvalue()
@app.get("/bad_image_stream")
def get_bad_image_stream():
image_bytes: bytes = generate_cat_picture()
# ❌ ¡No hagas esto!
image_stream = BytesIO(image_bytes)
return StreamingResponse(content=image_stream, media_type="image/png")
¿Por qué esto es un problema? Hay varias razones:
-
Ineficiencia con
BytesIO: Cuando le pasas unBytesIOaStreamingResponse, este tiende a iterar sobre el objeto buscando 'líneas' (separadas por\n). Esto no tiene sentido para datos binarios de una imagen. La imagen ya está completa en memoria; no hay necesidad de 'chunkearla' así. -
Transfer-Encoding: chunkedinnecesario:StreamingResponseusualmente implica el headerTransfer-Encoding: chunked. Esto le dice al cliente que el servidor enviará la respuesta en trozos, y que no se conoce el tamaño total de antemano. Para una imagen, cuyo tamaño sí se conoce (ya que la tenemos completa en memoria o en disco), esto es una complicación inútil. -
Problemas para el cliente: Si el cliente no sabe el tamaño total del archivo, no puede mostrar una barra de progreso. Esto puede ser una pequeña molestia para imágenes, pero una gran frustración para descargas más grandes.
-
Complejidad injustificada: Estás introduciendo una capa de abstracción y un patrón de diseño (streaming) cuando una solución más simple y directa (
ResponseoFileResponse) haría el trabajo de forma más eficiente y clara.
He visto casos donde la gente usa StreamingResponse para servir imágenes de OpenCV (cv2) o Pillow, pasándoles un BytesIO. Si bien técnicamente funciona, estás usando una herramienta para un propósito para el que no está optimizada, agregando complejidad sin ganar nada. Es sobreingeniería pura.
La StreamingResponse tiene su lugar, claro que sí. Por ejemplo, si tuvieras que servir un archivo CSV gigantesco que se genera fila por fila y no cabe todo en memoria, o un stream de un micro-servicio externo. Pero para imágenes fijas, ni lo pienses.
En resumen: Manténlo simple
Si la imagen está en memoria (generada, leída de DB, etc.), usa Response. Si la imagen está en disco, usa FileResponse. Es así de simple. Estas dos opciones cubren el 99% de los casos cuando trabajas con imágenes en FastAPI y te darán el mejor rendimiento y la menor complejidad.
El error más común que veo es la fascinación por la 'flexibilidad' de StreamingResponse, aplicándola donde no corresponde. No por tener una herramienta más avanzada significa que debas usarla en todas partes. Entiende el problema que resuelves y elige la herramienta adecuada. No te compliques la vida ni se la compliques al cliente sin una buena razón técnica.