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:- Backend: AWS Lambda functions with TypeScript
- Database: DynamoDB for data storage
- Queue: SQS for background jobs
- API: Function URLs with CORS for frontend access
Project Setup
npm install alchemy
npm install @aws-sdk/client-dynamodb @aws-sdk/client-sqs
npm install -D @types/aws-lambda
See the AWS Lambda example for the complete infrastructure setup:
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();
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" }),
};
}
}
API Endpoints
Once deployed, your API supports:POST /items- Create a new itemGET /items- List all itemsGET /items/:id- Get a specific itemDELETE /items/:id- Delete an item