AWS SDK S3 - Cargas de archivos, descargas y URLs presignadas con S3 en Next.js
Receta
npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner// lib/s3.ts
import { S3Client } from "@aws-sdk/client-s3";
export const s3Client = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});// app/api/upload/route.ts
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3Client } from "@/lib/s3";
export async function POST(req: Request) {
const { filename, contentType } = await req.json();
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: `uploads/${Date.now()}-${filename}`,
ContentType: contentType,
});
const url = await getSignedUrl(s3Client, command, { expiresIn: 600 });
return Response.json({ url });
}Cuándo usarlo: Necesitas cargar, descargar o administrar archivos en Amazon S3 desde una aplicación Next.js, usando URLs presignadas para cargas seguras del lado del cliente.
Ejemplo en funcionamiento
// app/components/FileUploader.tsx
"use client";
import { useState, useRef } from "react";
export default function FileUploader() {
const [uploading, setUploading] = useState(false);
const [uploadedUrl, setUploadedUrl] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
async function handleUpload() {
const file = fileInputRef.current?.files?.[0];
if (!file) return;
setUploading(true);
try {
// Paso 1: Obtén la URL presignada de nuestra API
const res = await fetch("/api/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
filename: file.name,
contentType: file.type,
}),
});
const { url } = await res.json();
// Paso 2: Carga directamente a S3 usando la URL presignada
await fetch(url, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
// Extrae la URL permanente (sin parámetros de consulta)
const permanentUrl = url.split("?")[0];
setUploadedUrl(permanentUrl);
} catch (error) {
console.error("La carga falló:", error);
} finally {
setUploading(false);
}
}
return (
<div className="p-6 max-w-md mx-auto space-y-4">
<div>
<input
ref={fileInputRef}
type="file"
accept="image/*,.pdf,.doc,.docx"
className="block w-full text-sm"
/>
</div>
<button
onClick={handleUpload}
disabled={uploading}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{uploading ? "Cargando..." : "Cargar"}
</button>
{uploadedUrl && (
<p className="text-sm text-green-600">
Cargado: <a href={uploadedUrl} className="underline">Ver archivo</a>
</p>
)}
</div>
);
}// app/actions/s3-actions.ts
"use server";
import {
ListObjectsV2Command,
GetObjectCommand,
DeleteObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { s3Client } from "@/lib/s3";
const BUCKET = process.env.S3_BUCKET_NAME!;
export async function listFiles(prefix: string = "uploads/") {
const command = new ListObjectsV2Command({
Bucket: BUCKET,
Prefix: prefix,
MaxKeys: 50,
});
const response = await s3Client.send(command);
return (
response.Contents?.map((item) => ({
key: item.Key!,
size: item.Size!,
lastModified: item.LastModified!.toISOString(),
})) ?? []
);
}
export async function getDownloadUrl(key: string) {
const command = new GetObjectCommand({
Bucket: BUCKET,
Key: key,
});
return getSignedUrl(s3Client, command, { expiresIn: 3600 });
}
export async function deleteFile(key: string) {
const command = new DeleteObjectCommand({
Bucket: BUCKET,
Key: key,
});
await s3Client.send(command);
}Lo que esto demuestra:
- Generación de URL presignada para cargas seguras del lado del cliente
- Carga directa a S3 sin pasar por tu servidor (sin límites de tamaño de archivo de Next.js)
- Server Actions para listar, descargar y eliminar objetos
- Separación adecuada de la configuración del cliente S3
Análisis profundo
Cómo funciona
- El AWS SDK v3 usa una arquitectura modular: instala solo los clientes que necesites (
@aws-sdk/client-s3) - Los comandos siguen el patrón de comando: crea un objeto de comando y luego envíalo con
send()a través del cliente - Las URLs presignadas permiten a los clientes cargar o descargar directamente a/desde S3 sin exponer credenciales de AWS
@aws-sdk/s3-request-presignergenera URLs firmadas con límite de tiempo para cualquier comando de S3- Las credenciales se resuelven desde variables de entorno, roles de IAM o la cadena de credenciales automáticamente
- Las claves de S3 (rutas) usan
/como delimitador, pero S3 es almacenamiento plano — las carpetas son solo prefijos de clave
Variaciones
Carga de archivo de Server Action (para archivos más pequeños):
"use server";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "@/lib/s3";
export async function uploadFile(formData: FormData) {
const file = formData.get("file") as File;
const buffer = Buffer.from(await file.arrayBuffer());
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: `uploads/${Date.now()}-${file.name}`,
Body: buffer,
ContentType: file.type,
});
await s3Client.send(command);
}Descarga y transmisión de un archivo:
// app/api/download/[key]/route.ts
import { GetObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "@/lib/s3";
export async function GET(
req: Request,
{ params }: { params: { key: string } }
) {
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: decodeURIComponent(params.key),
});
const response = await s3Client.send(command);
const stream = response.Body as ReadableStream;
return new Response(stream, {
headers: {
"Content-Type": response.ContentType ?? "application/octet-stream",
"Content-Length": String(response.ContentLength),
},
});
}Copiar objetos entre buckets:
import { CopyObjectCommand } from "@aws-sdk/client-s3";
const command = new CopyObjectCommand({
Bucket: "destination-bucket",
CopySource: "source-bucket/path/to/file.pdf",
Key: "new-path/file.pdf",
});
await s3Client.send(command);Notas de TypeScript
- Todos los comandos aceptan interfaces de entrada tipadas:
PutObjectCommandInput,GetObjectCommandInput, etc. GetObjectCommandOutput.Bodyestá tipado comoStreamingBlobPayloadOutputTypes— convierte aReadableStreamen entornos sin servidor- Usa el tipo
S3ClientConfigpara configuración personalizada del cliente
import type {
PutObjectCommandInput,
ListObjectsV2CommandOutput,
} from "@aws-sdk/client-s3";
const params: PutObjectCommandInput = {
Bucket: "my-bucket",
Key: "file.txt",
Body: "Hello, World!",
};Trampas
-
Errores de CORS en carga del lado del cliente — El navegador bloquea las solicitudes PUT a S3. Solución: Configura CORS en el bucket de S3 para permitir PUT desde tu dominio. Añade
AllowedOrigins,AllowedMethods: ["PUT"]yAllowedHeaders: ["Content-Type"]. -
URL presignada expirada — Las URLs expiran después de los segundos configurados en
expiresIn. Solución: Genera URLs justo antes de usarlas. Por defecto, 600 segundos (10 minutos) para cargas; no generes URLs con mucha anticipación. -
Cargas de archivos grandes fallando — Los archivos de más de 5 GB no pueden usar PUT único. Solución: Usa carga multiparte con la clase
Uploadde@aws-sdk/lib-storagepara archivos superiores a 100 MB. -
Content-Type faltante — Los archivos cargados sin Content-Type obtienen
application/octet-stream. Solución: Siempre pasaContentTypeenPutObjectCommandy en los encabezadosfetchdel lado del cliente. -
Límite de tamaño del body de Next.js — Las cargas de Server Action están limitadas al límite de tamaño del body de Next.js (por defecto 1 MB). Solución: Usa cargas de URL presignada para archivos más grandes o aumenta
experimental.serverActions.bodySizeLimitennext.config.js. -
Exposición de credenciales — Nunca importes
@aws-sdk/client-s3en componentes de cliente. Solución: Todo el uso de AWS SDK debe estar en rutas de API, Server Components o Server Actions.
Alternativas
| Librería | Mejor para | Compensación |
|---|---|---|
| @aws-sdk/client-s3 | Acceso completo a la API de S3 | Requiere cuenta de AWS y configuración de CORS |
| Vercel Blob | Almacenamiento simple de archivos en Vercel | Solo Vercel, menos control |
| Uploadthing | Cargas de archivos con hooks de React | Abstracción sobre S3, menos flexibilidad |
| Cloudflare R2 | Compatible con S3, sin cuotas de salida | SDK separado o modo de compatibilidad S3 |
| Supabase Storage | Integrado con Supabase | Ligado al ecosistema de Supabase |
Preguntas frecuentes
¿Qué es una URL presignada y por qué debería usar una para cargas?
- Una URL presignada es una URL firmada con límite de tiempo que otorga acceso temporal a una operación específica de S3
- El cliente carga directamente a S3, sin pasar por tu servidor y evitando límites de tamaño del body
- Las credenciales de AWS nunca se exponen al navegador
- Controlas el tiempo de expiración (por ejemplo, 600 segundos) y la operación permitida (PUT, GET)
¿Cómo funciona el flujo de carga de URL presignada de dos pasos?
- El cliente envía el nombre de archivo y el tipo de contenido a tu ruta de API
- El servidor genera una URL PUT presignada a través de
getSignedUrly la devuelve - El cliente usa
fetch(url, { method: "PUT", body: file })para cargar directamente a S3 - El archivo llega a S3 sin pasar nunca a través de tu servidor Next.js
Trampa: ¿Por qué obtengo errores de CORS al cargar desde el navegador a S3?
- Los buckets de S3 bloquean solicitudes de origen cruzado por defecto
- Debes configurar una política de CORS en el bucket permitiendo
PUTdesde tu dominio - Incluye
AllowedHeaders: ["Content-Type"]y tu dominio enAllowedOrigins - La configuración de CORS se establece en la configuración del bucket de S3, no en el código de tu aplicación
¿Cómo cargo archivos de más de 5 GB a S3?
PutObjectCommandúnico está limitado a 5 GB- Usa
@aws-sdk/lib-storagey su claseUploadpara cargas multiparte - La carga multiparte divide el archivo en partes, las carga en paralelo y las ensambla en S3
- Se recomienda para cualquier archivo superior a 100 MB
¿Cuál es el límite de tamaño del body de Next.js y cómo afecta a las cargas de Server Action?
- Los Server Actions por defecto tienen un límite de tamaño de body de solicitud de 1 MB
- Cargar archivos a través de un Server Action envía el archivo a través de tu servidor
- Para archivos más grandes, usa cargas de URL presignada en su lugar
- Alternativamente, aumenta
experimental.serverActions.bodySizeLimitennext.config.js
¿Cómo transmito una descarga de archivo desde S3 a través de una ruta de API?
const command = new GetObjectCommand({ Bucket: BUCKET, Key: key });
const response = await s3Client.send(command);
return new Response(response.Body as ReadableStream, {
headers: {
"Content-Type": response.ContentType ?? "application/octet-stream",
"Content-Length": String(response.ContentLength),
},
});Trampa: ¿Por qué mi archivo cargado tiene application/octet-stream como Content-Type?
- Si omites
ContentTypeenPutObjectCommand, S3 por defecto usaapplication/octet-stream - Siempre pasa
ContentType: file.typetanto en el comando como en los encabezadosfetchdel lado del cliente - Esto asegura que los navegadores puedan mostrar o manejar el archivo correctamente al descargarlo
¿Cómo tipo las entradas de comando de S3 en TypeScript?
import type { PutObjectCommandInput } from "@aws-sdk/client-s3";
const params: PutObjectCommandInput = {
Bucket: "my-bucket",
Key: "file.txt",
Body: "Hello, World!",
ContentType: "text/plain",
};Cada comando tiene un tipo *CommandInput y *CommandOutput correspondiente.
¿Cómo manejo el tipo GetObjectCommandOutput.Body en TypeScript?
Bodyestá tipado comoStreamingBlobPayloadOutputTypes, no comoReadableStream- En entornos sin servidor / ruta de API, conviértelo:
response.Body as ReadableStream - Siempre verifica si hay
undefinedantes de usar el body
¿Por qué nunca debería importar @aws-sdk/client-s3 en un componente de cliente?
- Los componentes de cliente envían JavaScript al navegador
- Importar el SDK de AWS en un componente de cliente agruparía credenciales de AWS en el cliente
- Todo el uso de AWS SDK debe mantenerse en rutas de API, Server Components o Server Actions
¿Cómo listo objetos en una "carpeta" específica de S3?
const command = new ListObjectsV2Command({
Bucket: BUCKET,
Prefix: "uploads/images/",
MaxKeys: 50,
});
const response = await s3Client.send(command);
const files = response.Contents ?? [];S3 es almacenamiento plano -- las "carpetas" son solo prefijos de clave separados por /.
¿Cómo elimino un archivo de S3 usando un Server Action?
"use server";
import { DeleteObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "@/lib/s3";
export async function deleteFile(key: string) {
await s3Client.send(
new DeleteObjectCommand({ Bucket: BUCKET, Key: key })
);
}Relacionado
- Next.js Server Actions — Operaciones S3 del lado del servidor
- AWS SDK Lambda — Activar Lambda para procesamiento de archivos
- Prisma — Almacenar metadatos de archivo en base de datos