Serverless on AWS without infrastructure boilerplate

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 r…


This content originally appeared on DEV Community and was authored by Aníbal Jorquera

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:

  1. Write business logic in TypeScript
  2. Switch context to define infrastructure in YAML, HCL, or CDK constructs
  3. 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:

  1. The code IS the infrastructure — decorators describe what you need, the framework generates the rest
  2. 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.


This content originally appeared on DEV Community and was authored by Aníbal Jorquera


Print Share Comment Cite Upload Translate Updates
APA

Aníbal Jorquera | Sciencx (2026-03-16T23:10:36+00:00) Serverless on AWS without infrastructure boilerplate. Retrieved from https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/

MLA
" » Serverless on AWS without infrastructure boilerplate." Aníbal Jorquera | Sciencx - Monday March 16, 2026, https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/
HARVARD
Aníbal Jorquera | Sciencx Monday March 16, 2026 » Serverless on AWS without infrastructure boilerplate., viewed ,<https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/>
VANCOUVER
Aníbal Jorquera | Sciencx - » Serverless on AWS without infrastructure boilerplate. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/
CHICAGO
" » Serverless on AWS without infrastructure boilerplate." Aníbal Jorquera | Sciencx - Accessed . https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/
IEEE
" » Serverless on AWS without infrastructure boilerplate." Aníbal Jorquera | Sciencx [Online]. Available: https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/. [Accessed: ]
rf:citation
» Serverless on AWS without infrastructure boilerplate | Aníbal Jorquera | Sciencx | https://www.scien.cx/2026/03/16/serverless-on-aws-without-infrastructure-boilerplate/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.