Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 020 — Servir archivos estáticos#

⬅️ 019 · 📚 Parte 1 · 🎓 Clases · 021 ➡️ Parte 1 — Responder · Nivel 🟢 introductorio · Pista backendClase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Entregar un archivo del disco con el tipo y la caché correctos, y entender por qué exponer un directorio es una decisión de seguridad.

🧩 La situación#

GET /estatico/logo.svg devuelve el archivo con content-type: image/svg+xml y una directiva de caché. Un archivo inexistente responde 404.

📖 Las tres decisiones que esconde#

Servir estáticos parece una línea de configuración. Son tres decisiones:

1. Qué directorio se expone. Todo lo que haya dentro es público, incluido lo que alguien deje ahí mañana. Un .env o una copia de seguridad en esa carpeta se sirven igual que el logo.

2. Cuánto se puede cachear. Sin Cache-Control, el navegador revalida en cada carga: se pierde casi toda la ventaja de servir un archivo estático. Con un máximo muy largo, publicar una versión nueva no llega a quien ya la tiene.

La respuesta habitual a esa tensión es el nombre con huella: logo.a3f9.svg con un año de caché. Si el contenido cambia, cambia el nombre, y el nombre nuevo no está en ninguna caché. La ficha de Vite explica cómo lo generan las herramientas de compilación.

3. Quién lo sirve en producción. Tu proceso de aplicación no es el mejor sitio para entregar archivos: un servidor especializado o una red de distribución lo hace mejor y libera hilos para lo que solo tú puedes hacer.

🧮 El contrato#

Petición Respuesta
GET /estatico/logo.svg 200
igual content-type: image/svg+xml
igual cache-control contiene max-age=3600
GET /estatico/no-existe.svg 404

El segundo caso comprueba que el tipo se deduce de la extensión. Los cuatro frameworks traen una tabla de tipos y aciertan con SVG, que no es de los más comunes.

<!-- generado: fichas -->

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
ASP.NET Core framework web de .NET (C#) 2016 MIT Microsoft y .NET Foundation

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
publico/logo.svg archivo del proyecto
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
publico/logo.svg archivo del proyecto
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-020-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
publico/logo.svg archivo del proyecto
src/main/java/labs/Aplicacion.java código Java
src/main/resources/publico/logo.svg archivo del proyecto

🔧 ASP.NET Core#

Reescritura multiplataforma y de código abierto de la pila web de Microsoft. Sus API mínimas trajeron el estilo de los microframeworks al ecosistema .NET.

Preparar sus dependencias, dentro de su directorio:

dotnet build -c Release --nologo -v quiet

Arrancarla suelta, sin el verificador:

PORT=3000 dotnet run -c Release --no-build --urls http://127.0.0.1:3000

Qué hay dentro de su directorio:

Archivo Qué es
Clase020.csproj proyecto de .NET: el marco de destino y las dependencias
Program.cs código C#
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
publico/logo.svg archivo del proyecto

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Servir un archivo es fácil en los cuatro. Lo que separa a las cuatro implementaciones son las decisiones que hay que tomar aparte: la caché, los archivos ocultos y de dónde salen los ficheros.

Express · express/server.mjs#

app.use(
  "/estatico",
  express.static(path.join(import.meta.dirname, "publico"), {
    maxAge: "1h",
    dotfiles: "deny",
    index: false,
  }),
);

Las tres opciones son las tres decisiones, escritas y a la vista.

dotfiles: "deny" es la que casi nadie pone, y es la que evita una fuga: sin ella, un .env que acabe en esa carpeta se sirve al primero que lo pida. Es la clase de agujero que aparece por exponer un directorio entero en lugar de archivos concretos.

maxAge se traduce a Cache-Control: public, max-age=3600. Sin ella el navegador revalida en cada carga y se pierde casi toda la ventaja de servir estáticos [rfc9111].

FastAPI · fastapi/main.py#

app.mount("/estatico", StaticFiles(directory=RAIZ), name="estatico")

Una línea. Y sin Cache-Control: StaticFiles no lo emite, así que la implementación tiene que añadir una capa que lo ponga:

@app.middleware("http")
async def cachear(peticion: Request, siguiente):
    respuesta: Response = await siguiente(peticion)
    if peticion.url.path.startswith("/estatico"):
        respuesta.headers["cache-control"] = "public, max-age=3600"
    return respuesta

Es el mejor ejemplo del elenco de la diferencia entre «funciona» y «está bien»: montar los estáticos funciona de inmediato, y servirlos con caché es una decisión que hay que tomar por separado — y que nadie recuerda hasta que mira las cabeceras.

Spring Boot · spring-boot/…/Aplicacion.java#

            registro.addResourceHandler("/estatico/**")
                    .addResourceLocations("classpath:/publico/")
                    .setCacheControl(CacheControl.maxAge(Duration.ofHours(1)).cachePublic());

La diferencia de fondo está en dos palabras: classpath: en vez de una ruta del disco. Los archivos van dentro del artefacto, no junto a él.

Eso hace el despliegue de una sola pieza —un .jar que contiene la aplicación y sus estáticos, sin nada que copiar aparte— y a cambio publicar un cambio de logo exige recompilar. Es un intercambio real y va en las dos direcciones: menos partes móviles en producción, más lento el ciclo de un cambio trivial.

CacheControl.maxAge(...).cachePublic() es además el único del elenco donde la directiva se construye con un tipo en vez de con una cadena.

ASP.NET Core · aspnet-core/Program.cs#

app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(Path.Combine(app.Environment.ContentRootPath, "publico")),
    RequestPath = "/estatico",
    OnPrepareResponse = contexto =>
    {
        contexto.Context.Response.Headers.CacheControl = "public, max-age=3600";
    },
});

La más verbosa de las cuatro, y la más flexible por un motivo concreto: FileProvider es una abstracción. PhysicalFileProvider lee del disco, y hay otros que leen de recursos incrustados en el ensamblado o de un almacenamiento remoto — sin cambiar el resto del código.

Es la misma idea que el classpath: de Spring, generalizada: de dónde salen los bytes es una decisión enchufable en vez de una convención fija.

OnPrepareResponse es un gancho por respuesta, así que la caché se decide archivo a archivo si hace falta — algo que la opción maxAge de Express, que es global, no permite.

🔬 Comparación#

Framework Líneas ¿Caché por omisión? Origen de los archivos
FastAPI 1 no disco
Express 5 no, pero opción directa disco
Spring Boot 3 configurable en la misma llamada dentro del artefacto
ASP.NET Core 8 no, con enganche para ponerla proveedor conectable

Ninguno de los cuatro emite Cache-Control por omisión. Es coherente —el framework no sabe cuánto vale tu archivo— y significa que la configuración por omisión de los cuatro es la lenta.

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 020

🧪 Reto de transferencia#

Añade un archivo .env de mentira a la carpeta pública y comprueba en las cuatro implementaciones si se sirve. Después corrige las que lo sirvan. Es el experimento más útil de esta clase.

🔗 Enlaces#

Fuentes#