Building serverless applications on AWS is incredibly powerful, but the developer experience is often fragmented.
To build a simple backend, you usually need to wire together API Gateway, Lambda, SQS, DynamoDB, Step Functions, S3, EventBridge, IAM roles, and more. Even with tools like AWS CDK or Terraform, you still spend a significant amount of time defining infrastructure instead of focusing on application logic.
I kept asking myself: What if you could define your entire serverless application in TypeScript — and let the infrastructure be generated automatically?
That's why I built Lafken.
Let's build something real
Imagine you're building an order management system. You need:
- An API to create and query orders
- A queue to process payments asynchronously
- An event bus to notify other services when an order is placed
- A workflow to orchestrate the fulfillment process
- A table to store orders
- A bucket to store invoices
- A scheduled job to clean up expired orders
In a traditional setup, you'd spend hours defining each resource, wiring permissions, and connecting everything. With Lafken, you describe each piece with a decorator and the framework takes care of the rest.
Storing orders
import { Table, PartitionKey, SortKey, Field, type PrimaryPartition } from '@lafken/dynamo/main';
import { createRepository } from '@lafken/dynamo/service';
@Table({
name: 'orders',
indexes: [
{ type: 'global', name: 'by_status', partitionKey: 'status', sortKey: 'createdAt' },
],
ttl: 'expiresAt',
})
export class Order {
@PartitionKey(String)
orderId: PrimaryPartition<string>;
@SortKey(String)
customerId: PrimaryPartition<string>;
@Field()
status: string;
@Field()
total: number;
@Field()
createdAt: string;
@Field()
expiresAt: number;
}
export const orderRepository = createRepository(Order);
This generates a DynamoDB table with a global secondary index, TTL configuration, and the correct attribute definitions. No CloudFormation, no Terraform — just a class.
Receiving orders through an API
import { Api, Get, Post, Event, ApiRequest, PathParam, BodyParam, PathParam, ContextParam } from '@lafken/api/main';
import { orderRespository } from '../tables/order';
@ApiRequest()
class BasePayload {
@ContextParam({
name: 'authorizer.principalId'
})
customerId: string;
}
@ApiRequest()
class CreateOrderPayload extends BasePayload {
@BodyParam({ min: 1 })
total: number;
}
@ApiRequest()
class GetPayload extends BasePayload {
@PathParam()
id: number;
}
@Api({ path: '/orders' })
export class OrderApi {
@Post()
create(@Event(CreateOrderPayload) e: CreateOrderPayload) {
const id = `oc-${new Date().getTime()}`;
// send event or message queue
return orderRespository.create({
orderId: id,
customerId: e.customerId,
total: e.total,
//...
});
}
@Get({ path: '/{id}' })
getById(@Event(GetPayload) e: GetPayload) {
return orderRespository.findOne({
keyCondition: {
partition: {
orderId: e.id,
},
sort: {
customerId: e.customerId,
},
},
});
}
}
Each method becomes a Lambda function behind API Gateway. Request validation is built-in through the @BodyParam and @PathParam decorators.
Processing payments asynchronously
import { Queue, Standard, Fifo, Payload, Param, Event } from '@lafken/queue/main';
interface MessageBody {
orderId: string;
customerId: string;
// ...
}
@Payload()
class PaymentMessage {
@Param({ source: 'body', parse: true })
message: MessageBody;
}
@Queue()
export class PaymentQueue {
@Fifo({ queueName: 'payment-processing', contentBasedDeduplication: true })
process(@Event(PaymentMessage) message: PaymentMessage) {
// Messages are processed in exact send order
// No duplicate payments
}
}
This creates a FIFO SQS queue, a Lambda consumer, and the event source mapping between them. The @Payload decorator maps message attributes to typed properties.
Reacting to events
import { EventRule, Rule, Event } from '@lafken/event/main';
@EventRule()
export class OrderEvents {
@Rule({
pattern: {
source: 'order-service',
detailType: ['order.placed'],
},
})
onOrderPlaced(@Event() event: any) {
// Notify warehouse, send confirmation email, update analytics...
}
@Rule({
integration: 's3',
pattern: {
detailType: ['Object Created'],
detail: {
bucket: { name: ['order-invoices'] },
object: { key: [{ prefix: 'invoices/' }] },
},
},
})
onInvoiceUploaded(@Event() event: any) {
// Process the invoice automatically
}
}
Each @Rule listens to specific EventBridge patterns. You can react to custom events, S3 notifications, DynamoDB streams — all with the same decorator.
Orchestrating fulfillment
import { StateMachine, Task } from '@lafken/state-machine/main';
@StateMachine({
name: 'order-fulfillment',
startAt: 'validatePayment'
})
export class OrderFulfillment {
@Task({ next: 'reserveInventory' })
validatePayment() { /* ... */ }
@Task({ next: 'shipOrder' })
reserveInventory() { /* ... */ }
@Task({ end: true })
shipOrder() { /* ... */ }
}
Each @Task becomes a Lambda function, and the entire class becomes an AWS Step Functions state machine.
Storing invoices
import { Bucket } from '@lafken/bucket/main';
@Bucket({
name: 'order-invoices',
versioned: true,
eventBridgeEnabled: true,
lifeCycleRules: {
'archived/': {
expiration: { days: 365 },
transitions: [
{ days: 30, storage: 'standard_ia' },
{ days: 90, storage: 'glacier' },
],
},
},
})
export class OrderInvoicesBucket {}
Cleaning up automatically
import { Schedule, Cron } from '@lafken/schedule/main';
@Schedule()
export class OrderMaintenance {
@Cron({ schedule: 'cron(0 3 * * ? *)' })
cleanupExpiredOrders() {
// Runs every day at 3:00 AM UTC
}
@Cron({ schedule: { hour: 0, minute: 0, weekDay: 'SUN' } })
generateWeeklyReport() {
// Runs every Sunday at midnight
}
}
Putting it all together
All these resources are grouped into a module and wired through createApp:
import { createApp, createModule } from '@lafken/main';
import { ApiResolver } from '@lafken/api/resolver';
import { EventRuleResolver } from '@lafken/event/resolver';
import { StateMachineResolver } from '@lafken/state-machine/resolver';
import { BucketResolver } from '@lafken/bucket/resolver';
import { DynamoResolver } from '@lafken/dynamo/resolver';
import { QueueResolver } from '@lafken/queue/resolver';
import { ScheduleResolver } from '@lafken/schedule/resolver';
// ... resource imports
const orderModule = createModule({
name: 'order',
resources: [OrderApi, OrderEvents, OrderFulfillment, PaymentQueue, OrderMaintenance],
});
createApp({
name: 'order-app',
resolvers: [
new ApiResolver(),
new EventRuleResolver({ busName: 'order-events' }),
new StateMachineResolver(),
new BucketResolver([OrderInvoicesBucket]),
new DynamoResolver([OrderTable]),
new QueueResolver(),
new ScheduleResolver(),
],
modules: [orderModule],
});
That's the entire application. Seven AWS services, fully configured, from a single TypeScript codebase.
Why does Lafken exist?
Every time I started a new serverless project, I found myself repeating the same cycle:
- Write business logic in TypeScript
- Switch context to define infrastructure in YAML, HCL, or CDK constructs
- Debug deployment errors that had nothing to do with my application
The friction wasn't in any single step — it was in the constant context switching between writing application code and configuring infrastructure. I wanted to stay in TypeScript the entire time.
I wanted a framework where:
- The code IS the infrastructure — decorators describe what you need, the framework generates the rest
- You think in modules, not stacks — group related resources logically, not by AWS service type
How it works under the hood
Lafken doesn't hide Terraform — it generates it for you using cdk-terrain.
TypeScript Code (with decorators)
↓
Module (groups resources)
↓
App + Resolvers (processes decorators)
↓
Terraform Configuration (generated automatically)
- Decorators capture metadata about your classes and methods using TypeScript reflection
- Modules group related resources into logical units
- Resolvers read that metadata and generate real infrastructure via cdk-terrain
- createApp() orchestrates everything and produces a Terraform plan ready to deploy
The output is standard Terraform. That means:
You can inspect the generated plan before deploying
You can integrate Lafken into an existing Terraform workflow
You keep all the benefits of Terraform state management
There's no vendor lock-in to a proprietary deployment engine
Getting started
npm create lafken@latest
The create command walks you through a series of questions to scaffold your project with the right packages and configuration.
Documentation and repository
Lafken is under active development, and the best place to follow updates is the GitHub repository.
Repository & documentation: https://github.com/hero64/lafken
If you want to contribute, feedback and PRs are welcome.
You write TypeScript. Lafken generates Terraform. AWS does the rest.

Top comments (0)