Compartir en Twitter
Go to Homepage

APRENDE SERVERLESS CON AWS EN 7 PROYECTOS PRÁCTICOS

November 7, 2025

Introducción al Desarrollo Serverless con AWS

El desarrollo serverless ha transformado la forma en que los programadores construyen aplicaciones escalables y eficientes. Con AWS, puedes crear aplicaciones sin preocuparte por la gestión de servidores, lo que permite centrarte en el código y la funcionalidad. Este tutorial presenta siete proyectos prácticos para aprender a utilizar servicios como AWS Lambda, API Gateway, DynamoDB, EventBridge, Cognito, SES, SNS y WebSockets. Cada proyecto está diseñado para desarrollar habilidades específicas, desde la creación de APIs hasta sistemas complejos de comercio electrónico. A continuación, exploraremos cada proyecto con ejemplos de código y explicaciones detalladas para que puedas aplicarlos en tus propios desarrollos.

Proyecto de API Combinada

Este proyecto introduce los fundamentos del desarrollo serverless mediante la creación de una API que combina datos de múltiples fuentes externas. Por ejemplo, puedes crear una API que obtenga ofertas de juegos de Steam y las convierta a tu moneda local o traduzca noticias a tu idioma. Utilizarás AWS Lambda y API Gateway para construir la API, aprendiendo a manejar parámetros y realizar solicitudes HTTP a APIs externas.

La arquitectura es sencilla: una función Lambda recibe parámetros a través de una URL, como https://apiurl.amazonaws.com/dev/steamdeals?currency=EUR, y combina datos de dos APIs externas. El flujo incluye obtener datos de una API, procesarlos y devolverlos en el formato de API Gateway.

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import axios from "axios";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const currency = event.queryStringParameters?.currency || "USD";
    const steamDeals = await axios.get("https://api.steam.com/deals");
    const exchangeRates = await axios.get(
        `https://api.exchangerate.com/rates?base=USD&target=${currency}`
    );

    const convertedDeals = steamDeals.data.map((deal) => ({
        ...deal,
        price: deal.price * exchangeRates.data.rate,
    }));

    return {
        statusCode: 200,
        body: JSON.stringify(convertedDeals),
    };
};

Recomendamos usar el Serverless Framework o AWS CDK para gestionar la infraestructura, ya que son herramientas que optimizan la configuración y despliegue de recursos en AWS. Puedes encontrar APIs públicas en recursos como el repositorio de GitHub de APIs públicas para practicar.

Proyecto de Acortador de URLs

En este proyecto, aprenderás a trabajar con DynamoDB, creando una tabla para almacenar URLs y dos puntos finales de API: uno para agregar una URL y otro para recuperar la original a partir de un código corto. Este ejercicio te familiarizará con operaciones de escritura y lectura en DynamoDB, así como con la configuración de variables de entorno.

La API para agregar URLs genera un código de cinco caracteres y lo almacena en DynamoDB junto con la URL original. La URL resultante se devuelve como https://apiurl.amazonaws.com/dev/get/{code}.

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
import { v4 as uuidv4 } from "uuid";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const { url } = JSON.parse(event.body || "{}");
    const code = uuidv4().slice(0, 5);

    await dynamoDb
        .put({
            TableName: TABLE_NAME,
            Item: { id: code, url },
        })
        .promise();

    const apiUrl = process.env.API_URL;
    return {
        statusCode: 200,
        body: JSON.stringify({ shortenedUrl: `${apiUrl}/get/${code}` }),
    };
};

Para configurar la variable de entorno apiUrl en Serverless Framework, puedes usar el siguiente fragmento en tu archivo serverless.yml:

environment:
    apiUrl:
        Fn::Join:
            - ""
            - - "https://"
              - Ref: HttpApi
              - ".execute-api.${self:provider.region}.amazonaws.com"

El segundo endpoint recupera la URL original buscando el código en DynamoDB. Opcionalmente, puedes configurar una redirección 301 para enviar al usuario directamente a la URL original.

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const code = event.pathParameters?.code;
    const result = await dynamoDb
        .get({
            TableName: TABLE_NAME,
            Key: { id: code },
        })
        .promise();

    if (!result.Item) {
        return {
            statusCode: 404,
            body: JSON.stringify({ error: "URL not found" }),
        };
    }

    return {
        statusCode: 301,
        headers: { Location: result.Item.url },
        body: "",
    };
};

Proyecto de Aplicación de Recordatorios

Este proyecto introduce el uso de índices secundarios globales (GSI) y la funcionalidad de Time-To-Live (TTL) en DynamoDB, además de integrar Amazon SES para correos electrónicos o SNS para mensajes de texto. También puedes crear una interfaz web simple alojada en S3 con CloudFront.

La aplicación permite a los usuarios crear recordatorios que se almacenan en DynamoDB con un TTL, que indica cuándo se eliminará el registro. Una función Lambda se activa al eliminarse un registro para enviar el recordatorio por correo o SMS. La tabla incluye un GSI con userId como clave de partición y TTL como clave de ordenación.

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const { userId, message, notificationType, remindAt } = JSON.parse(
        event.body || "{}"
    );
    const ttl = Math.floor(new Date(remindAt).getTime() / 1000);

    await dynamoDb
        .put({
            TableName: TABLE_NAME,
            Item: {
                id: uuidv4(),
                userId,
                TTL: ttl,
                notificationType,
                message,
            },
        })
        .promise();

    return {
        statusCode: 200,
        body: JSON.stringify({ message: "Reminder created" }),
    };
};

Para enviar el recordatorio, configura una función Lambda que se active con un evento de DynamoDB Stream:

import { DynamoDBStreamEvent } from "aws-lambda";
import { SES, SNS } from "aws-sdk";

const ses = new SES();
const sns = new SNS();

export const handler = async (event: DynamoDBStreamEvent) => {
    for (const record of event.Records) {
        if (record.eventName === "REMOVE") {
            const { userId, notificationType, message } =
                record.dynamodb.NewImage;
            if (notificationType === "email") {
                await ses
                    .sendEmail({
                        Destination: { ToAddresses: [userId] },
                        Message: {
                            Body: { Text: { Data: message } },
                            Subject: { Data: "Reminder" },
                        },
                        Source: "[email protected]",
                    })
                    .promise();
            } else if (notificationType === "sms") {
                await sns
                    .publish({
                        Message: message,
                        PhoneNumber: userId,
                    })
                    .promise();
            }
        }
    }
};

El segundo endpoint permite obtener todos los recordatorios de un usuario mediante una consulta al GSI:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const userId = event.queryStringParameters?.userId;
    const result = await dynamoDb
        .query({
            TableName: TABLE_NAME,
            IndexName: "UserIdIndex",
            KeyConditionExpression: "userId = :userId",
            ExpressionAttributeValues: { ":userId": userId },
        })
        .promise();

    return {
        statusCode: 200,
        body: JSON.stringify(result.Items),
    };
};

Para la interfaz, puedes crear una aplicación web simple y usar el plugin serverless-s3-sync para desplegarla en S3.

Proyecto de Aplicación de Chat en Vivo

Este proyecto te enseña a construir aplicaciones con WebSockets utilizando AWS API Gateway. Los usuarios pueden crear o unirse a salas de chat y enviar mensajes que se distribuyen a todos los conectados en la sala. La arquitectura utiliza DynamoDB para almacenar conexiones y mensajes, con un GSI para consultar usuarios por roomId.

Los tipos de mensajes incluyen connectToRoom, createRoom, onMessage y disconnect. El flujo para connectToRoom verifica si la sala existe antes de registrar al usuario.

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const connectToRoom = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const { roomId, connectionId } = JSON.parse(event.body || "{}");
    const result = await dynamoDb
        .query({
            TableName: TABLE_NAME,
            IndexName: "RoomIdIndex",
            KeyConditionExpression: "roomId = :roomId",
            ExpressionAttributeValues: { ":roomId": roomId },
        })
        .promise();

    if (!result.Items.length) {
        return {
            statusCode: 404,
            body: JSON.stringify({ error: "Room not found" }),
        };
    }

    await dynamoDb
        .put({
            TableName: TABLE_NAME,
            Item: { connectionId, roomId },
        })
        .promise();

    return { statusCode: 200, body: JSON.stringify({ message: "Connected" }) };
};

Para enviar mensajes, la función onMessage consulta todos los usuarios en la sala y envía el mensaje a través de WebSocket:

import { APIGatewayProxyEvent } from "aws-lambda";
import { ApiGatewayManagementApi, DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const apiGateway = new ApiGatewayManagementApi({
    endpoint: process.env.WEBSOCKET_ENDPOINT,
});

export const handler = async (event: APIGatewayProxyEvent) => {
    const { connectionId, message } = JSON.parse(event.body || "{}");
    const user = await dynamoDb
        .get({
            TableName: process.env.TABLE_NAME,
            Key: { connectionId },
        })
        .promise();

    const users = await dynamoDb
        .query({
            TableName: process.env.TABLE_NAME,
            IndexName: "RoomIdIndex",
            KeyConditionExpression: "roomId = :roomId",
            ExpressionAttributeValues: { ":roomId": user.Item.roomId },
        })
        .promise();

    for (const user of users.Items) {
        await apiGateway
            .postToConnection({
                ConnectionId: user.connectionId,
                Data: JSON.stringify({ message }),
            })
            .promise();
    }

    return { statusCode: 200, body: "" };
};

La función disconnect elimina el registro del usuario de DynamoDB cuando se desconecta.

Proyecto de Aplicación de Votación de Ideas

Este proyecto introduce el uso de AWS Cognito para autenticación y diseños avanzados de tablas DynamoDB. Construirás una aplicación donde los usuarios pueden crear tableros de ideas, agregar ideas y votar por ellas, asegurando que cada usuario vote solo una vez.

La tabla de votos utiliza dos índices para permitir consultas de votos por idea y por usuario:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const voteForIdea = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const { ideaId, userId } = JSON.parse(event.body || "{}");
    const existingVote = await dynamoDb
        .query({
            TableName: TABLE_NAME,
            KeyConditionExpression: "pk = :ideaId and sk = :userId",
            ExpressionAttributeValues: { ":ideaId": ideaId, ":userId": userId },
        })
        .promise();

    if (existingVote.Items.length) {
        return {
            statusCode: 400,
            body: JSON.stringify({ error: "User already voted" }),
        };
    }

    await dynamoDb
        .put({
            TableName: TABLE_NAME,
            Item: { pk: ideaId, sk: userId, pk2: userId, sk2: ideaId },
        })
        .promise();

    return {
        statusCode: 200,
        body: JSON.stringify({ message: "Vote recorded" }),
    };
};

Para obtener los detalles de un tablero, consultas las ideas y sus votos:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const boardId = event.pathParameters?.boardId;
    const ideas = await dynamoDb
        .query({
            TableName: TABLE_NAME,
            KeyConditionExpression: "pk = :boardId",
            ExpressionAttributeValues: { ":boardId": boardId },
        })
        .promise();

    const result = await Promise.all(
        ideas.Items.map(async (idea) => {
            const votes = await dynamoDb
                .query({
                    TableName: TABLE_NAME,
                    KeyConditionExpression: "pk = :ideaId",
                    ExpressionAttributeValues: { ":ideaId": idea.id },
                })
                .promise();
            return { ...idea, voteCount: votes.Items.length };
        })
    );

    return { statusCode: 200, body: JSON.stringify(result) };
};

Proyecto de Aplicación de Mensajería

Este proyecto combina WebSockets, Cognito y claves compuestas en DynamoDB para crear una aplicación de mensajería donde los usuarios solicitan unirse a grupos, y el propietario aprueba o rechaza las solicitudes. Los mensajes se almacenan y pueden recuperarse al iniciar sesión.

La tabla de membresía utiliza claves compuestas para consultas eficientes:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const joinGroupRequest = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const { groupId, userId } = JSON.parse(event.body || "{}");
    await dynamoDb
        .put({
            TableName: TABLE_NAME,
            Item: { pk: groupId, sk: `joinRequest#${userId}` },
        })
        .promise();

    return {
        statusCode: 200,
        body: JSON.stringify({ message: "Join request created" }),
    };
};

Para enviar mensajes, se consulta a los miembros del grupo y se usa WebSocket para distribuir el mensaje, almacenándolo en DynamoDB:

import { APIGatewayProxyEvent } from "aws-lambda";
import { ApiGatewayManagementApi, DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const apiGateway = new ApiGatewayManagementApi({
    endpoint: process.env.WEBSOCKET_ENDPOINT,
});

export const handler = async (event: APIGatewayProxyEvent) => {
    const { groupId, message, userId } = JSON.parse(event.body || "{}");
    const members = await dynamoDb
        .query({
            TableName: process.env.TABLE_NAME,
            KeyConditionExpression: "pk = :groupId and begins_with(sk, :user)",
            ExpressionAttributeValues: {
                ":groupId": groupId,
                ":user": "user#",
            },
        })
        .promise();

    await dynamoDb
        .put({
            TableName: process.env.TABLE_NAME,
            Item: { pk: groupId, sk: `message#${Date.now()}`, message, userId },
        })
        .promise();

    for (const member of members.Items) {
        await apiGateway
            .postToConnection({
                ConnectionId: member.connectionId,
                Data: JSON.stringify({ message, userId }),
            })
            .promise();
    }

    return { statusCode: 200, body: "" };
};

Proyecto de Sistema de Comercio Electrónico Basado en Eventos

Este proyecto utiliza AWS EventBridge para manejar eventos como la creación de pedidos, empaquetado y envío en un sistema de comercio electrónico. También refuerza el diseño de tablas DynamoDB y el uso de SES y SNS para notificaciones.

Los productos se almacenan con claves de ordenación jerárquicas para permitir filtrado:

import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();
const TABLE_NAME = process.env.TABLE_NAME || "";

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const category = event.queryStringParameters?.category || "clothing";
    const subCategory = event.queryStringParameters?.subCategory || "mens";
    const result = await dynamoDb
        .query({
            TableName: TABLE_NAME,
            KeyConditionExpression:
                "pk = :category and begins_with(sk, :subCategory)",
            ExpressionAttributeValues: {
                ":category": category,
                ":subCategory": subCategory,
            },
        })
        .promise();

    return { statusCode: 200, body: JSON.stringify(result.Items) };
};

Para los pedidos, se crea un evento en EventBridge que activa dos funciones Lambda: una para enviar el pedido al almacén y otra para notificar al cliente.

import { EventBridge } from "aws-sdk";
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

const eventBridge = new EventBridge();

export const handler = async (
    event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
    const order = JSON.parse(event.body || "{}");
    await eventBridge
        .putEvents({
            Entries: [
                {
                    Source: "ecommerce",
                    DetailType: "orderCreated",
                    Detail: JSON.stringify(order),
                },
            ],
        })
        .promise();

    return {
        statusCode: 200,
        body: JSON.stringify({ message: "Order created" }),
    };
};

Conclusiones

Estos siete proyectos ofrecen una ruta práctica para dominar el desarrollo serverless con AWS, cubriendo desde APIs básicas hasta sistemas complejos impulsados por eventos. Al implementar cada proyecto, aprenderás a integrar servicios como Lambda, DynamoDB, EventBridge, Cognito, SES, SNS y WebSockets, adquiriendo habilidades esenciales para construir aplicaciones modernas y escalables. Comienza con los proyectos iniciales si eres principiante, o avanza hacia los más avanzados si ya tienes experiencia, y lleva tus habilidades de desarrollo al siguiente nivel.