En este post aprenderemos a conectar nuestras aplicaciones NextJs con los buckets de AWS S3
Antes de iniciar recomiendo leer
Escenario con el que trabajaremos
En los archivos de
Estructura recomendada
project/
│
├── app/
│ ├── api/
│ │ └── b/[...path]/route.ts ← proxy hacia S3
│ └── page.tsx ← ejemplo de uso
│
├── lib/
│ └── s3.ts ← helper AWS S3
│
├── .env.local
├── next.config.mjs
└── package.json
Crear el proyecto Next.js
Comienza creando tu aplicación Next.js con soporte para TypeScript:
npx create-next-app@latest next-prisma --typescript
Esto generará la estructura básica de Next.js con TypeScript.
cd next-prisma
Next.js con AWS S3
Instalar el SDK de AWS
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner
Configurar variables de entorno .env.local
AWS_ACCESS_KEY_ID=TU_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=TU_SECRET
AWS_REGION=us-east-1
AWS_S3_BUCKET=tu-bucket
Crear un helper para S3
Creamos en el directorio el siguiente archivo lib/s3.js en donde colocamos:
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function getPresignedUrl(key: string) {
const command = new GetObjectCommand({
Bucket: process.env.AWS_S3_BUCKET,
Key: key,
});
const url = await getSignedUrl(s3Client, command, { expiresIn: 3600 }); // 1 hora
return url;
}
Crea la ruta que genera el acceso a las imágenes
Procedemos a crear archivo route con la siguiente estructura /app/b/[...path]/route.ts donde colocamos
import { NextResponse } from "next/server";
import { getPresignedUrl } from "@/lib/s3";
export async function GET(
req: Request,
context: { params: Promise<{ path: string[] }> }
) {
try {
const { path } = await context.params; // 👈 desestructura el params correctamente
const fullPath = path.join("/"); // oscardevops/images/2025/08/imagen.webp
const bucket = process.env.AWS_BUCKET_NAME!;
const key = fullPath.replace(`${bucket}/`, ""); // images/2025/08/imagen.webp
// Generar URL firmada
const signedUrl = await getPresignedUrl(key);
// Descargar la imagen desde S3 usando fetch
const response = await fetch(signedUrl);
if (!response.ok) {
return NextResponse.json(
{ error: "No se pudo descargar la imagen desde S3" },
{ status: 404 }
);
}
// Convertir a bytes y devolver la respuesta
const arrayBuffer = await response.arrayBuffer();
const contentType = response.headers.get("content-type") || "image/jpeg";
return new NextResponse(arrayBuffer, {
headers: {
"Content-Type": contentType,
"Cache-Control": "public, max-age=3600",
},
});
} catch (error) {
console.error("Error al obtener la imagen desde S3:", error);
return NextResponse.json(
{ error: "Error interno al obtener la imagen" },
{ status: 500 }
);
}
}
Nota: En versiones recientes de Next.js, el handler debe declarar params como una promesa y usar await para obtener los valores.
| Antes (Next.js 14 o menor) | Ahora (Next.js 15 o superior) |
|---|---|
params llegaba como objeto directo { path: string[] } |
params llega como Promise<{ path: string[] }> |
Acceso directo: params.path |
Necesitas hacer const { path } = await context.params |
¿Qué hace este flujo?
- Cuando tu navegador pide:
http://localhost:3000/b/oscardevops/images/2025/08/17551018070831755101807083circuitolm35raspberrypipico.webp - Next.js llama a
/app/b/[...path]/route.ts - El backend genera una URL firmada temporal
- Descarga la imagen desde S3
- La devuelve directamente (con los headers correctos)
- El usuario nunca ve la URL real de S3
next.config.mjs
Para que Next.js permita optimizar imágenes que provienen del backend:
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "http",
hostname: "localhost",
port: "3000",
pathname: "/b/**",
},
],
},
};
export default nextConfig;
✅ Resultado
- Tu imagen se carga desde la misma ruta:
http://localhost:3000/b/oscardevops/images/2025/08/17551018070831755101807083circuitolm35raspberrypipico.webp - El bucket sigue privado
- La URL firmada se maneja solo en el servidor
- Tu usuario nunca ve ni necesita permisos de AWS