AWS SDK Lambda - Invoca funciones AWS Lambda desde Next.js con payloads tipados y manejo de errores
Receta
npm install @aws-sdk/client-lambda// lib/lambda.ts
import { LambdaClient } from "@aws-sdk/client-lambda";
export const lambdaClient = new LambdaClient({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});// lib/invoke-lambda.ts
import { InvokeCommand } from "@aws-sdk/client-lambda";
import { lambdaClient } from "./lambda";
export async function invokeLambda<TInput, TOutput>(
functionName: string,
payload: TInput
): Promise<TOutput> {
const command = new InvokeCommand({
FunctionName: functionName,
Payload: new TextEncoder().encode(JSON.stringify(payload)),
});
const response = await lambdaClient.send(command);
if (response.FunctionError) {
const errorPayload = JSON.parse(
new TextDecoder().decode(response.Payload)
);
throw new Error(
`Lambda error: ${errorPayload.errorMessage ?? response.FunctionError}`
);
}
return JSON.parse(new TextDecoder().decode(response.Payload)) as TOutput;
}Cuándo usarlo: Necesitas invocar funciones AWS Lambda desde una aplicación Next.js para procesamiento en segundo plano, cómputo pesado o acceso a servicios nativos de AWS.
Ejemplo de trabajo
// app/components/ImageProcessor.tsx
"use client";
import { useState } from "react";
import { processImage } from "../actions/image-actions";
export default function ImageProcessor() {
const [result, setResult] = useState<{
thumbnailUrl: string;
dimensions: { width: number; height: number };
} | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function handleProcess(formData: FormData) {
setLoading(true);
setError(null);
try {
const data = await processImage(formData);
setResult(data);
} catch (err) {
setError(err instanceof Error ? err.message : "Processing failed");
} finally {
setLoading(false);
}
}
return (
<div className="max-w-md mx-auto p-6 space-y-4">
<form action={handleProcess}>
<input name="imageUrl" placeholder="Image URL" required className="w-full border rounded px-3 py-2" />
<input name="width" type="number" placeholder="Target width" defaultValue={200} className="w-full border rounded px-3 py-2 mt-2" />
<button
type="submit"
disabled={loading}
className="mt-3 bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{loading ? "Processing..." : "Process Image"}
</button>
</form>
{error && <p className="text-red-600">{error}</p>}
{result && (
<div>
<p>Thumbnail: {result.dimensions.width}x{result.dimensions.height}</p>
<img src={result.thumbnailUrl} alt="Processed thumbnail" className="border rounded" />
</div>
)}
</div>
);
}// app/actions/image-actions.ts
"use server";
import { invokeLambda } from "@/lib/invoke-lambda";
interface ProcessImageInput {
imageUrl: string;
targetWidth: number;
}
interface ProcessImageOutput {
thumbnailUrl: string;
dimensions: { width: number; height: number };
}
export async function processImage(formData: FormData) {
const imageUrl = formData.get("imageUrl") as string;
const targetWidth = Number(formData.get("width")) || 200;
const result = await invokeLambda<ProcessImageInput, ProcessImageOutput>(
"image-processor",
{ imageUrl, targetWidth }
);
return result;
}Qué demuestra esto:
- Invocación de Lambda con seguridad de tipos con tipos genéricos de entrada/salida
- Server Actions envolviendo llamadas Lambda para usar en formularios de cliente
- Manejo de errores para errores de funciones Lambda
- Codificación/decodificación de payload con TextEncoder/TextDecoder
Profundización
Cómo funciona
- El cliente Lambda del AWS SDK v3 envía solicitudes HTTP a la API de Lambda para invocar funciones
InvokeCommandenvía el payload comoUint8Arrayy recibe la respuesta de la misma manera- La invocación síncrona (predeterminada) espera a que la función se complete y devuelve el resultado
- El campo
FunctionErroren la respuesta indica si la función Lambda lanzó un error - Los payloads se limitan a 6MB para invocaciones síncronas y 256KB para asíncronas
- Las funciones Lambda se ejecutan en su propio runtime (Node.js, Python, etc.) independiente de tu aplicación Next.js
Variaciones
Invocación asíncrona (fire-and-forget):
import { InvokeCommand, InvocationType } from "@aws-sdk/client-lambda";
const command = new InvokeCommand({
FunctionName: "email-sender",
InvocationType: InvocationType.Event, // async, devuelve 202 inmediatamente
Payload: new TextEncoder().encode(
JSON.stringify({ to: "user@example.com", subject: "Welcome!" })
),
});
await lambdaClient.send(command); // Devuelve inmediatamente, sin payload de respuestaEjecución en seco (validar permisos e entrada):
const command = new InvokeCommand({
FunctionName: "my-function",
InvocationType: InvocationType.DryRun,
Payload: new TextEncoder().encode(JSON.stringify({ test: true })),
});
// Devuelve 204 si es válido, lanza excepción si los permisos son incorrectos
await lambdaClient.send(command);Listar funciones disponibles:
import { ListFunctionsCommand } from "@aws-sdk/client-lambda";
const command = new ListFunctionsCommand({ MaxItems: 50 });
const response = await lambdaClient.send(command);
const functions = response.Functions?.map((fn) => ({
name: fn.FunctionName,
runtime: fn.Runtime,
memory: fn.MemorySize,
lastModified: fn.LastModified,
}));Invocar con cualificador (versión o alias):
const command = new InvokeCommand({
FunctionName: "my-function",
Qualifier: "production", // nombre de alias o número de versión
Payload: new TextEncoder().encode(JSON.stringify(payload)),
});Notas de TypeScript
InvokeCommandOutput.PayloadesUint8Array | undefined— siempre verifica indefinido antes de decodificar- Crea funciones wrapper tipadas con genéricos para cada función Lambda que llames
InvocationTypeenum proporcionaRequestResponse(síncrono),Event(asíncrono) yDryRun
import type { InvokeCommandOutput } from "@aws-sdk/client-lambda";
function decodePayload<T>(response: InvokeCommandOutput): T | null {
if (!response.Payload) return null;
return JSON.parse(new TextDecoder().decode(response.Payload)) as T;
}Trampas
-
Codificación de payload — Pasar una cadena o objeto simple como
Payloadlanza un error de tipo. Solución: Siempre codifica connew TextEncoder().encode(JSON.stringify(data))y decodifica respuestas connew TextDecoder().decode(response.Payload). -
Latencia de arranque en frío — Las primeras invocaciones después de períodos inactivos tardan varios segundos. Solución: Usa concurrencia aprovisionada para funciones sensibles a la latencia, o acepta arranques en frío y muestra estados de carga en la UI.
-
Desajuste de timeout — Las funciones Lambda tienen un timeout predeterminado de 3 segundos, pero el procesamiento puede tardar más. Solución: Establece el timeout de la función Lambda apropiadamente en AWS. También considera invocación asíncrona para tareas de larga duración.
-
Fallos silenciosos con invocación asíncrona —
InvocationType: "Event"devuelve 202 sin detalles de error. Solución: Configura una Dead Letter Queue (DLQ) en la función Lambda para capturar invocaciones asíncronas fallidas. -
Límite de tamaño de payload — Los payloads síncronos se limitan a 6MB. Solución: Para datos más grandes, almacénalos en S3 y pasa la clave de S3 en el payload.
-
Permisos faltantes — El usuario o rol de IAM necesita permiso
lambda:InvokeFunction. Solución: Añade el permiso a tu política de IAM para el ARN de función específico.
Alternativas
| Librería | Mejor para | Compromiso |
|---|---|---|
| @aws-sdk/client-lambda | Invocación directa de Lambda | Requiere configuración de AWS, permisos IAM |
| Next.js API Routes | Lógica del lado del servidor en la misma aplicación | Tiempo de ejecución limitado, recursos compartidos |
| Vercel Functions | Serverless en Vercel | Específico de Vercel, menos control |
| AWS API Gateway + Lambda | Puntos finales de API pública | Más infraestructura para administrar |
| Step Functions | Orquestar múltiples Lambdas | Mayor complejidad, costo adicional |
Preguntas frecuentes
¿Por qué necesito codificar el payload con TextEncoder al invocar una Lambda?
InvokeCommandesperaPayloadcomoUint8Array, no un objeto simple o cadena- Usa
new TextEncoder().encode(JSON.stringify(data))para convertir - Las respuestas también regresan como
Uint8Arrayy necesitannew TextDecoder().decode() - Pasar un objeto simple lanza un error de tipo en tiempo de ejecución
¿Cuál es la diferencia entre invocación de Lambda síncrona y asíncrona?
- Síncrona (predeterminada): Espera a que la función termine y devuelve el resultado
- Asíncrona (
InvocationType: "Event"): Devuelve HTTP 202 inmediatamente, sin payload de respuesta - Usa síncrona cuando necesites el resultado de inmediato
- Usa asíncrona para tareas fire-and-forget como enviar correos electrónicos
¿Cómo manejo errores de una función Lambda?
const response = await lambdaClient.send(command);
if (response.FunctionError) {
const error = JSON.parse(
new TextDecoder().decode(response.Payload)
);
throw new Error(error.errorMessage);
}El campo FunctionError se establece cuando Lambda lanza; el payload contiene detalles del error.
Trampa: ¿Por qué mi invocación Lambda asíncrona falla silenciosamente sin ningún error?
InvocationType: "Event"devuelve 202 sin detalles de error incluso si la función falla- Configura una Dead Letter Queue (DLQ) en la Lambda para capturar invocaciones fallidas
- Usa CloudWatch Logs para depurar la ejecución de la función
¿Cuáles son los límites de tamaño de payload para invocación de Lambda?
- Invocaciones síncronas: límite de payload de 6MB
- Invocaciones asíncronas: límite de payload de 256KB
- Para datos más grandes, almacénalos en S3 y pasa la clave de S3 en el payload
Trampa: ¿Por qué es lenta mi invocación de Lambda en la primera llamada?
- Las funciones Lambda experimentan "arranques en frío" después de períodos inactivos
- El runtime debe inicializarse, cargar dependencias y ejecutar código de inicialización
- Los arranques en frío pueden tardar varios segundos dependiendo del runtime y el tamaño del paquete
- Usa concurrencia aprovisionada para funciones sensibles a la latencia
¿Cómo creo una función wrapper de Lambda con seguridad de tipos en TypeScript?
async function invokeLambda<TInput, TOutput>(
functionName: string,
payload: TInput
): Promise<TOutput> {
const command = new InvokeCommand({
FunctionName: functionName,
Payload: new TextEncoder().encode(JSON.stringify(payload)),
});
const response = await lambdaClient.send(command);
return JSON.parse(
new TextDecoder().decode(response.Payload)
) as TOutput;
}¿Cómo tipifiqué InvokeCommandOutput.Payload en TypeScript?
Payloadse tipifica comoUint8Array | undefined- Siempre verifica que no sea
undefinedantes de decodificar - Lanza el JSON decodificado con
as Tpara tu tipo de retorno esperado - El SDK no valida la forma de respuesta en tiempo de compilación
¿Cómo invoco una versión específica o alias de una función Lambda?
const command = new InvokeCommand({
FunctionName: "my-function",
Qualifier: "production", // alias o número de versión
Payload: new TextEncoder().encode(JSON.stringify(payload)),
});¿Qué permisos de IAM se requieren para invocar una Lambda desde mi aplicación Next.js?
- El usuario o rol de IAM necesita permiso
lambda:InvokeFunction - Limita el permiso al ARN de función específico por seguridad
- Usa
lambda:ListFunctionssi también necesitas listar funciones disponibles
¿Cómo envuelvo una llamada Lambda en una Next.js Server Action?
"use server";
import { invokeLambda } from "@/lib/invoke-lambda";
export async function processImage(formData: FormData) {
const imageUrl = formData.get("imageUrl") as string;
return invokeLambda("image-processor", { imageUrl });
}Server Actions mantienen credenciales de AWS en el servidor y proporcionan una API limpia para componentes de cliente.
¿Qué es una invocación DryRun y cuándo la usaría?
InvocationType: "DryRun"valida permisos e entrada sin ejecutar la función- Devuelve 204 si la llamada tendría éxito, lanza si los permisos son incorrectos
- Útil para probar la configuración de IAM antes de hacer invocaciones reales
Relacionado
- AWS SDK S3 — Pasa claves de S3 a Lambda para procesamiento de archivos
- AWS SDK Polly — Otra integración de servicio AWS
- Next.js Server Actions — Envolviendo llamadas Lambda en actions