Serverless Architecture Project Structure
Function-as-a-Service architecture with TypeScript. AWS Lambda, API Gateway, and infrastructure as code.
Project Directory
myproject/
package.json
tsconfig.json
sst.config.ts
SST infrastructure config
.gitignore
.env.example
README.md
packages/
Monorepo packages
functions/
Lambda handlers
package.json
tsconfig.json
src/
api/
HTTP API handlers
users/
create.ts
POST /users
get.ts
GET /users/:id
list.ts
GET /users
orders/
create.ts
get.ts
events/
Event-triggered handlers
user-created.ts
EventBridge handler
order-placed.ts
scheduled/
Cron jobs
daily-report.ts
cleanup.ts
queues/
SQS consumers
email-queue.ts
notification-queue.ts
core/
Shared business logic
package.json
tsconfig.json
src/
index.ts
Public exports
domain/
user/
user.ts
User entity
user.service.ts
order/
order.ts
order.service.ts
repositories/
user.repository.ts
order.repository.ts
events/
publisher.ts
EventBridge publisher
types.ts
infra/
Infrastructure stacks
api.ts
API Gateway routes
database.ts
DynamoDB tables
events.ts
EventBridge rules
queues.ts
SQS queues
storage.ts
S3 buckets
tests/
unit/
services/
integration/
api/
Why This Structure?
This structure separates Lambda handlers (thin entry points) from shared business logic in core/. Each handler imports from core, keeping functions small. Infrastructure is defined as code in infra/, and the monorepo structure with SST enables type-safe resource binding.
Key Directories
- packages/functions/-Lambda handlers organized by trigger type (api, events, queues)
- packages/core/-Shared domain logic, services, and repositories
- infra/-SST/CDK stacks defining AWS resources
- src/api/-One file per endpoint: create.ts, get.ts, list.ts
Lambda Handler
// packages/functions/src/api/users/create.ts
import { UserService } from "@myproject/core";
import { ApiHandler } from "sst/node/api";
export const handler = ApiHandler(async (event) => {
const body = JSON.parse(event.body ?? "{}");
const user = await UserService.create(body);
return { statusCode: 201, body: JSON.stringify(user) };
});
When To Use This
- Unpredictable or spiky traffic patterns
- Pay-per-use cost model preferred
- No server management overhead
- Event-driven workloads (S3, SQS, EventBridge)
- APIs with independent endpoint scaling
Lambda Triggers
- API Gateway-HTTP endpoints → Lambda functions
- EventBridge-Async events between services
- SQS-Queue processing with retry and DLQ
- Scheduled-Cron jobs via CloudWatch Events
Trade-offs
- Cold starts-First invocation has latency, mitigate with provisioned concurrency
- Execution limits-15 min max runtime, 10GB memory, 6MB payload
- Vendor lock-in-AWS-specific services, harder to migrate
Testing Strategy
- Unit tests-Test core services with mocked repositories
- Handler tests-Test Lambda handlers with event fixtures
- Integration-Use SST's
sst devfor live Lambda testing
Best Practices
- Keep handlers thin—delegate to core services
- Use environment variables for configuration
- Implement idempotency for event handlers
- Set up dead-letter queues for failed events
- Use structured logging with correlation IDs
Naming Conventions
- Handlers-Match route:
users/create.ts→ POST /users - Stacks-By resource type:
api.ts,database.ts - Events-Past tense:
user-created,order-placed