StackPractices
intermediate Por Mathias Paulenko

Construir APIs Serverless con API Gateway

Cómo diseñar, desplegar y gestionar APIs HTTP serverless usando AWS API Gateway, Lambda y patrones de function-as-a-service.

Visión general

La computación serverless te permite ejecutar código sin provisionar ni gestionar servidores. AWS Lambda ejecuta tus funciones en respuesta a eventos, y API Gateway provee la puerta de entrada HTTP que enruta las peticiones a esas funciones. Juntas forman una plataforma de API que escala automáticamente y se paga por petición, eliminando el costo de servidores ociosos.

Esta arquitectura es ideal para APIs con tráfico variable o impredecible. Una startup podría servir cien peticiones por día en el lanzamiento y un millón por día seis meses después — serverless maneja ambos sin planificación de capacidad. Los trade-offs son la latencia de cold start (el retraso cuando una función despierta tras estar inactiva) y herramientas específicas del proveedor que crean vendor lock-in.

Cuándo usarlo

  • Construyes APIs con patrones de tráfico esporádicos o impredecibles. Consulta Rate Limiting para proteger APIs bajo carga.
  • Prototipas productos donde el servidor debería costar cero mientras nadie lo usa.
  • Procesas webhooks, uploads de archivos o eventos programados vía HTTP. Consulta Input Validation para validar peticiones entrantes.
  • Creas microservicios donde cada endpoint tiene necesidades de recursos diferentes.

No la uses para APIs con tráfico alto y sostenido, donde un contenedor cálido sale más barato, ni para trabajo de larga duración por encima del límite de 15 minutos de Lambda: eso va a ECS/Fargate o a un sistema batch.

Solución

AWS Lambda Handler (Python)

import json

def lambda_handler(event, context):
    # API Gateway pasa los datos HTTP en el objeto event
    method = event['httpMethod']
    path = event['path']
    query = event.get('queryStringParameters', {})

    if method == 'GET' and path == '/users':
        return {
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps({'users': ['alice', 'bob']})
        }

    if method == 'POST' and path == '/users':
        body = json.loads(event['body'])
        return {
            'statusCode': 201,
            'body': json.dumps({'id': '123', 'name': body['name']})
        }

    return {'statusCode': 404, 'body': json.dumps({'error': 'Not found'})}

AWS Lambda Handler (Node.js)

exports.handler = async (event) => {
  const { httpMethod, path, body } = event;

  if (httpMethod === 'GET' && path === '/users') {
    return {
      statusCode: 200,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ users: ['alice', 'bob'] })
    };
  }

  if (httpMethod === 'POST' && path === '/users') {
    try {
      const data = JSON.parse(body);
      return {
        statusCode: 201,
        body: JSON.stringify({ id: '123', name: data.name })
      };
    } catch {
      return { statusCode: 400, body: JSON.stringify({ error: 'Invalid JSON body' }) };
    }
  }

  return { statusCode: 404, body: JSON.stringify({ error: 'Not found' }) };
};

Sin ese try/catch, un body malformado lanza una excepción y API Gateway devuelve un 502 genérico: un 400 le dice al cliente qué hizo mal.

Despliegue con Terraform

Una API funcional necesita cinco piezas que los ejemplos mínimos suelen saltarse: el rol IAM, la integración AWS_PROXY, el permiso que permite a API Gateway invocar la función, y un despliegue con su stage.

resource "aws_iam_role" "lambda_role" {
  name = "users-lambda-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action    = "sts:AssumeRole"
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "basic" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_lambda_function" "handler" {
  function_name    = "users-handler"
  handler          = "index.handler"
  runtime          = "nodejs20.x"
  filename         = "function.zip"
  source_code_hash = filebase64sha256("function.zip")
  role             = aws_iam_role.lambda_role.arn
}

resource "aws_api_gateway_rest_api" "api" {
  name = "users-api"
}

resource "aws_api_gateway_resource" "users" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  parent_id   = aws_api_gateway_rest_api.api.root_resource_id
  path_part   = "users"
}

resource "aws_api_gateway_method" "get_users" {
  rest_api_id   = aws_api_gateway_rest_api.api.id
  resource_id   = aws_api_gateway_resource.users.id
  http_method   = "GET"
  authorization = "NONE"
}

resource "aws_api_gateway_integration" "get_users" {
  rest_api_id             = aws_api_gateway_rest_api.api.id
  resource_id             = aws_api_gateway_resource.users.id
  http_method             = aws_api_gateway_method.get_users.http_method
  type                    = "AWS_PROXY"
  integration_http_method = "POST"
  uri                     = aws_lambda_function.handler.invoke_arn
}

resource "aws_lambda_permission" "apigw" {
  statement_id  = "AllowAPIGatewayInvoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.handler.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
}

resource "aws_api_gateway_deployment" "deploy" {
  rest_api_id = aws_api_gateway_rest_api.api.id
  stage_name  = "prod"
  depends_on  = [aws_api_gateway_integration.get_users]
}

output "invoke_url" {
  value = "https://${aws_api_gateway_rest_api.api.id}.execute-api.${var.region}.amazonaws.com/prod"
}

AWS_PROXY es la decisión clave aquí: reenvía la petición HTTP completa a Lambda y espera la forma de respuesta {statusCode, headers, body} que devuelven los handlers — sin mapping templates que mantener.

Si aplicas esto y el endpoint devuelve 500 Internal Server Error, el sospechoso habitual es el aws_lambda_permission ausente: API Gateway necesita permiso explícito para invocar la función y el error que devuelve no lo dice. Revisa aws lambda get-policy en la función, y de paso busca Execution failed due to configuration error en los access logs de API Gateway.

Explicación

  • API Gateway: Recibe peticiones HTTP, maneja la terminación TLS, throttling, caching y autenticación, y luego invoca la función Lambda con los datos de la petición en un objeto event estructurado.
  • Lambda: Ejecución de funciones disparada por eventos, stateless por diseño. AWS gestiona el escalado — 1.000 peticiones simultáneas significan 1.000 instancias de la función en paralelo, y pagas por tiempo de ejecución y memoria, nada más.
  • Cold starts: Cuando una función no ha sido invocada recientemente, AWS inicializa una instancia del runtime desde cero — cuenta con 100 ms a 2 s extra en esa primera llamada según el runtime y la memoria asignada. La concurrencia provisionada mantiene funciones calientes para endpoints sensibles a latencia — consulta Cold Start Optimization para la guía completa.
  • Infrastructure as Code: Terraform, AWS SAM o el Serverless Framework expresan tus rutas de API, funciones, roles IAM y variables de entorno como configuración versionada en lugar de clics en la consola.
flowchart diagram: Cliente

Variantes

PlataformaAPI GatewayRuntime de funciónMejor para
AWSAPI Gateway + LambdaPython, Node, Java, GoPlataforma madura, integraciones amplias
AzureAPI Management + Functions.NET, Node, PythonEcosistema Microsoft, integración Visual Studio
GCPCloud Endpoints + Cloud FunctionsNode, Python, GoIntegración BigQuery, precio competitivo

API Gateway también soporta APIs WebSocket para tráfico bidireccional — consulta WebSockets en tiempo real — y Lambda authorizers para validación de tokens personalizada antes de que la petición llegue siquiera a tu handler.

Consideraciones de Producción

Las checklists genéricas para APIs serverless dicen siempre lo mismo; estas son las cosas que de verdad muerden en producción:

  • Autenticación: authorization = "NONE" vale para una demo y está mal para una API real. Adjunta un Lambda authorizer (ver la receta complementaria) o Cognito/IAM para cualquier cosa de cara al usuario; usa flujos OAuth 2.0 para clientes de terceros.
  • Secretos: nunca incrustes credenciales en el paquete de despliegue ni en variables de entorno en texto plano — usa AWS Secrets Manager (Azure Key Vault o Google Secret Manager en las otras nubes) y rótalos.
  • Idempotencia: los gateways y los clientes reintentan las peticiones fallidas te guste o no — un POST que no es idempotente crea duplicados que luego tienes que explicar. Clavea las escrituras en un token de idempotencia suministrado por el cliente o un ID de deduplicación.
  • Fallos parciales: con funciones disparadas por SQS, un solo registro malo falla el lote entero. Devuelve batchItemFailures para que solo se reintente el mensaje envenenado — y configura una dead letter queue para lo que sigue fallando.
  • Acceso a VPC: las funciones que tocan recursos privados necesitan configuración VPC, que cambia el comportamiento de cold start y cuesta un NAT gateway para salida a internet. No lo actives por defecto — añádelo cuando de verdad necesites subnets privadas.
  • Testing: simula las llamadas al SDK de AWS para los unit tests (SAM CLI o LocalStack emulan API Gateway + Lambda en local), corre integration tests contra un stage desplegado de verdad, y prueba carga antes del lanzamiento — la promesa de “escala automáticamente” oculta límites de throttling y concurrencia que solo aparecen bajo tráfico real.
  • Despliegues graduales: usa versiones y aliases de Lambda con canary traffic shifting (SAM lo soporta de forma nativa) en lugar de actualizar $LATEST en caliente.

Lo que funciona

  • Diseña sin estado: todo lo que tu función guarde en memoria puede desaparecer en la siguiente invocación — pasa el estado por la petición o por un almacén externo.
  • Minimiza el tamaño del paquete de despliegue: los paquetes grandes aumentan el tiempo de cold start.
  • Configura timeouts y memoria adecuados: la memoria escala la CPU proporcionalmente. Si una función es lenta, subir la memoria puede salir más barato que pagar más tiempo de ejecución a menos memoria.
  • Usa variables de entorno para la configuración: las URLs de base de datos y los feature flags deberían configurarse vía variables de entorno, no incrustarse en el paquete.
  • Implementa logging estructurado: escribe logs JSON con request IDs — CloudWatch Logs Insights puede consultarlos eficientemente para depurar.
  • Usa dead letter queues: las invocaciones asíncronas fallidas se reintentan automáticamente. Una DLQ captura los fallos persistentes para inspeccionarlos y reprocesarlos.

Errores comunes

  • Tratar Lambda como un servidor de larga duración: las funciones se cortan a los 15 minutos, a mitad del trabajo si hace falta. Mueve lo que tarde más a procesamiento batch (AWS Batch) o contenedores (ECS/Fargate).
  • Ignorar los cold starts: una API de cara al usuario con 3 segundos de cold start da una experiencia terrible. Usa concurrencia provisionada o un ping de keep-alive en los endpoints sensibles a latencia.
  • Sobre-provisionar memoria: la memoria escala linealmente con la factura, y la mayoría de los handlers jamás tocan los 1.024 MB que alguien tecleó el primer día. Perfila la tuya y asigna lo que realmente usa.
  • Hardcodear credenciales: nunca hagas commit de claves AWS o contraseñas de base de datos en tu repositorio.
  • No manejar fallos parciales: en procesamiento por lotes (triggers SQS), un solo registro malo puede tirar el lote entero.

Lectura adicional

Preguntas frecuentes

¿Cómo manejo las conexiones a base de datos en Lambda?

Usa connection pooling con un proxy ligero como RDS Proxy, o implementa tu propia lógica de reutilización de conexiones — abre la conexión una vez fuera del handler para que las invocaciones calientes la reaprovechen. Abrir una conexión nueva en cada invocación es lento y puede agotar el límite de conexiones de la base de datos.

¿Puedo ejecutar una aplicación full-stack en serverless?

Sí, pero evalúa los trade-offs. Los sitios estáticos y las APIs encajan de maravilla. Las conexiones WebSocket de larga duración o las sesiones stateful pueden servirse mejor con contenedores o EC2.

¿Es serverless más barato que los servidores tradicionales?

Todo se reduce a la forma de tu tráfico: con tráfico esporádico serverless sale barato porque pagas por petición, mientras que con tráfico alto y sostenido la balanza se inclina hacia contenedores provisionados o EC2. Modela tus volúmenes reales con la calculadora de precios de AWS antes de decidir — el punto de cruce sorprende a la mayoría de los equipos.

¿Cómo pruebo funciones Lambda en local?

Usa la AWS SAM CLI o el Serverless Framework para emular API Gateway y Lambda en local. Estas herramientas montan tu código en un contenedor Docker que reproduce el entorno de runtime de AWS.

¿Cuál es la diferencia entre las APIs REST y HTTP de API Gateway?

Las HTTP APIs son más nuevas, más baratas y más rápidas — soportan autorización OIDC/JWT y cuestan aproximadamente un tercio de las REST. Las REST APIs añaden funciones que las HTTP no tienen: API keys, usage plans, validación de peticiones, caching y endpoints privados. Para la mayoría de las APIs nuevas, empieza con HTTP API; recurre a REST cuando necesites esos extras.

¿Cómo añado autenticación a una API de API Gateway?

Tres caminos habituales: un Lambda authorizer (validación de tokens personalizada con tu propia lógica), Cognito user pools (identidad gestionada), o autenticación IAM para llamadas internas servicio a servicio. El enfoque de Lambda authorizer está cubierto en la receta complementaria.