Skip to main content

AWS Full-Stack Example

This example demonstrates building a complete full-stack serverless application on AWS. The example uses the same infrastructure as the AWS Lambda example but can be extended with additional frontend resources.

Features

  • Lambda Function: Serverless API endpoints
  • DynamoDB Table: NoSQL database for data persistence
  • SQS Queue: Async message processing
  • IAM Roles: Security and permissions
  • Function URLs: Direct HTTP access
  • CORS: Cross-origin resource sharing

Architecture

The full-stack application consists of:
  1. Backend: AWS Lambda functions with TypeScript
  2. Database: DynamoDB for data storage
  3. Queue: SQS for background jobs
  4. API: Function URLs with CORS for frontend access

Project Setup

1
Install Dependencies
2
npm install alchemy
npm install @aws-sdk/client-dynamodb @aws-sdk/client-sqs
npm install -D @types/aws-lambda
3
Create alchemy.run.ts
4
See the AWS Lambda example for the complete infrastructure setup:
5
import alchemy from "alchemy";
import { Function, Queue, Role, Table } from "alchemy/aws";
import { Bundle } from "alchemy/esbuild";
import path from "node:path";
import { fileURLToPath } from "node:url";

const app = await alchemy("aws-app");

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Create infrastructure resources in parallel
const [queue, table, role] = await Promise.all([
  Queue("queue", {
    queueName: `${app.name}-${app.stage}-queue`,
    visibilityTimeout: 30,
    messageRetentionPeriod: 345600,
  }),
  Table("table", {
    tableName: `${app.name}-${app.stage}-table`,
    partitionKey: { name: "id", type: "S" },
  }),
  Role("role", {
    roleName: `${app.name}-${app.stage}-lambda-role`,
    assumeRolePolicy: {
      Version: "2012-10-17",
      Statement: [
        {
          Effect: "Allow",
          Principal: { Service: "lambda.amazonaws.com" },
          Action: "sts:AssumeRole",
        },
      ],
    },
  }),
]);

// Bundle Lambda code
const bundle = await Bundle("api-bundle", {
  entryPoint: path.join(__dirname, "src", "index.ts"),
  outdir: ".out",
  format: "esm",
  platform: "node",
  target: "node20",
  minify: true,
  external: ["@aws-sdk/*"],
});

// Create Lambda function with Function URL
const api = await Function("api", {
  functionName: `${app.name}-${app.stage}-api`,
  bundle,
  roleArn: role.arn,
  handler: "index.handler",
  environment: {
    TABLE_NAME: table.tableName,
    QUEUE_URL: queue.url,
  },
  url: {
    authType: "NONE",
    cors: {
      allowOrigins: ["*"],
      allowMethods: ["GET", "POST", "PUT", "DELETE"],
      allowHeaders: ["content-type"],
    },
  },
});

console.log(`API URL: ${api.url}`);

await app.finalize();
6
Create API Handler
7
Create src/index.ts with full CRUD operations:
8
import {
  DynamoDBClient,
  PutItemCommand,
  GetItemCommand,
  ScanCommand,
  DeleteItemCommand,
} from "@aws-sdk/client-dynamodb";
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";

const dynamodb = new DynamoDBClient({});
const sqs = new SQSClient({});

const TABLE_NAME = process.env.TABLE_NAME!;
const QUEUE_URL = process.env.QUEUE_URL!;

export async function handler(
  event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> {
  const method = event.requestContext.http.method;
  const path = event.requestContext.http.path;

  try {
    // POST /items - Create new item
    if (method === "POST" && path === "/items") {
      const body = JSON.parse(event.body || "{}");
      const id = crypto.randomUUID();

      await dynamodb.send(
        new PutItemCommand({
          TableName: TABLE_NAME,
          Item: {
            id: { S: id },
            data: { S: JSON.stringify(body) },
            createdAt: { N: Date.now().toString() },
          },
        })
      );

      // Send to queue for async processing
      await sqs.send(
        new SendMessageCommand({
          QueueUrl: QUEUE_URL,
          MessageBody: JSON.stringify({ id, action: "created" }),
        })
      );

      return {
        statusCode: 201,
        body: JSON.stringify({ id, message: "Created" }),
      };
    }

    // GET /items/:id - Get specific item
    if (method === "GET" && path.startsWith("/items/")) {
      const id = path.split("/")[2];

      const result = await dynamodb.send(
        new GetItemCommand({
          TableName: TABLE_NAME,
          Key: { id: { S: id } },
        })
      );

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

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

    // GET /items - List all items
    if (method === "GET" && path === "/items") {
      const result = await dynamodb.send(
        new ScanCommand({ TableName: TABLE_NAME })
      );

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

    // DELETE /items/:id - Delete item
    if (method === "DELETE" && path.startsWith("/items/")) {
      const id = path.split("/")[2];

      await dynamodb.send(
        new DeleteItemCommand({
          TableName: TABLE_NAME,
          Key: { id: { S: id } },
        })
      );

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

    return {
      statusCode: 404,
      body: JSON.stringify({ error: "Route not found" }),
    };
  } catch (error) {
    console.error(error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: "Internal server error" }),
    };
  }
}
9
Deploy
10
Deploy the full-stack application:
11
npm exec tsx alchemy.run.ts

API Endpoints

Once deployed, your API supports:
  • POST /items - Create a new item
  • GET /items - List all items
  • GET /items/:id - Get a specific item
  • DELETE /items/:id - Delete an item

Frontend Integration

You can connect any frontend framework:
// Example fetch call
const response = await fetch('https://your-function-url.lambda-url.us-east-1.on.aws/items', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name: 'Example Item' }),
});

const data = await response.json();
console.log(data.id); // UUID of created item

Key Features Explained

Parallel Resource Creation

Create multiple resources concurrently for faster deployment:
const [queue, table, role] = await Promise.all([...]);

Environment Variables

Pass resource references to Lambda:
environment: {
  TABLE_NAME: table.tableName,
  QUEUE_URL: queue.url,
}

CORS Configuration

Enable frontend access:
cors: {
  allowOrigins: ["*"],
  allowMethods: ["GET", "POST", "PUT", "DELETE"],
  allowHeaders: ["content-type"],
}

Source Code

View the complete source code: examples/aws-app

Build docs developers (and LLMs) love