Skip to content

Why Effortless?

Serverless is the right model for shipping products. Pay per use, scale to zero, zero ops — Lambda, DynamoDB, SQS, S3, SES, CloudFront are proven, scalable services that handle everything from a side project to production traffic.

But delivering a serverless product is unreasonably hard.

The problem

Serverless services are simple individually. The hard part is wiring them into a product. Every product needs an API, a database, maybe a queue, file storage, email, and a frontend. Each is a separate AWS service — and connecting them means:

  • CloudFormation / CDK / Terraform stacks — even a simple app requires hundreds of lines of infrastructure config
  • IAM policies for every connection — your API Lambda needs DynamoDB access, your stream Lambda needs SQS access, your queue Lambda needs S3 access — each permission written manually
  • Event source mappings — wiring DynamoDB Streams to Lambda, SQS to Lambda, S3 events to Lambda — each with its own config
  • Environment variables — table names, queue URLs, bucket names passed between services
  • Deployment orchestration — minutes-long CloudFormation deploys, state files to manage, drift to detect

The code for each handler is 10–20 lines. The infrastructure around it is 10x that. You spend more time wiring services together than building the product itself.

With Effortless — ship the whole product

Same product, same AWS services — but you only write the parts that matter:

my-service/
├── src/
│ ├── orders.ts ← API + database + stream processing
│ ├── uploads.ts ← file storage + image processing
│ └── notifications.ts ← queue + email
├── effortless.config.ts ← project name, region, defaults
└── package.json

Each handler definition creates all the AWS resources it needs. deps wires them together — IAM permissions, environment variables, typed clients — automatically.

src/orders.ts
import { defineTable, defineApi, unsafeAs } from "effortless-aws";
type Order = { id: string; product: string; amount: number };
export const orders = defineTable({
schema: unsafeAs<Order>(),
onRecord: async ({ record }) => {
console.log("New order:", record.new!.product);
},
});
export const api = defineApi({
basePath: "/orders",
deps: () => ({ orders }),
post: async ({ req, deps }) => {
await deps.orders.put({
id: crypto.randomUUID(),
product: req.body.product,
amount: req.body.amount,
});
return { status: 201, body: { ok: true } };
},
});
Terminal window
eff deploy # ~10 seconds — the whole product

One command creates everything: DynamoDB table, stream processor Lambda, API Lambda with Function URL, IAM roles, environment variable wiring. No YAML. No state files. No IAM policy writing.

Everything a product needs

Your product needsEffortless handlerAWS resources created
REST APIdefineApiLambda + Function URL + IAM
DatabasedefineTableDynamoDB + optional stream Lambda
Background jobsdefineFifoQueueSQS FIFO + consumer Lambda
File storagedefineBucketS3 + optional event Lambda
Transactional emaildefineMailerSES + DKIM identity
Website / SSR appdefineAppCloudFront + Lambda + S3
Static site / SPAdefineStaticSiteCloudFront + S3

All in the same project, all deployed with one command, all with automatic IAM wiring between them.

// One project. One deploy. A complete product backend.
export const orders = defineTable({ schema: unsafeAs<Order>(), onRecord: processOrder });
export const uploads = defineBucket({ onObjectCreated: processImage });
export const queue = defineFifoQueue({ schema: unsafeAs<Job>(), onMessage: processJob });
export const mailer = defineMailer({ domain: "myapp.com" });
export const api = defineApi({
basePath: "/api",
deps: () => ({ orders, uploads, queue, mailer }),
// all deps are typed, all IAM permissions are automatic
});
export const site = defineStaticSite({ dir: "dist", build: "npm run build" });

You can deliver an entire serverless product from TypeScript alone — and get back to building the product itself.

Use cases

REST API with database

The most common Lambda pattern: HTTP endpoints that read/write from DynamoDB.

import { defineTable, defineApi, unsafeAs } from "effortless-aws";
import { z } from "zod";
type User = { id: string; email: string; name: string; createdAt: string };
export const users = defineTable({
schema: unsafeAs<User>(),
});
export const api = defineApi({
basePath: "/users",
deps: () => ({ users }),
get: {
"/{id}": async ({ req, deps }) => {
const user = await deps.users.get({ id: req.params.id });
if (!user) return { status: 404, body: { error: "Not found" } };
return { status: 200, body: user };
},
},
schema: (input: unknown) =>
z.object({ email: z.string(), name: z.string() }).parse(input),
post: async ({ data, deps }) => {
const user: User = {
id: crypto.randomUUID(),
email: data.email, // typed from schema
name: data.name,
createdAt: new Date().toISOString(),
};
await deps.users.put(user); // typed client, auto IAM
return { status: 201, body: user };
},
});

What you get without writing any infrastructure:

  • Schema validation — invalid requests rejected before your handler runs
  • Typed clientsdeps.users.put() knows the shape of User
  • Auto IAM — each Lambda gets exactly the DynamoDB permissions it needs
  • Table name wiring — no hardcoded ARNs or environment variable plumbing

Event-driven processing

DynamoDB streams let you react to data changes without polling.

import { defineTable, unsafeAs } from "effortless-aws";
type Order = { id: string; product: string; amount: number; status: string };
export const orders = defineTable({
schema: unsafeAs<Order>(),
// Stream processor — runs on every insert/update/delete
onRecord: async ({ record }) => {
if (record.eventName === "INSERT") {
// Send confirmation email, update analytics, notify warehouse
console.log(`New order: ${record.new!.product} — $${record.new!.amount}`);
}
},
});
// Or process records in batches for efficiency
type AnalyticsEvent = { id: string; event: string; timestamp: number };
export const analytics = defineTable({
schema: unsafeAs<AnalyticsEvent>(),
batchSize: 100,
onBatch: async ({ records }) => {
const inserts = records.filter(r => r.eventName === "INSERT");
await bulkIndexToElasticsearch(inserts.map(r => r.new!));
},
});

The stream, event source mapping, batch size config, and partial failure reporting are all handled automatically.

SSR framework deployment

Deploy Nuxt, Astro SSR, or any framework with server-side rendering — CloudFront CDN with Lambda Function URL for SSR and S3 for static assets.

import { defineApp, defineApi } from "effortless-aws";
// SSR app via CloudFront + Lambda Function URL
export const app = defineApp({
server: ".output/server",
assets: ".output/public",
build: "nuxt build",
domain: "app.example.com",
});
// API endpoints in the same project
export const api = defineApi({
basePath: "/api",
get: {
"/items": async () => {
return { status: 200, body: await fetchItems() };
},
},
});

Or for static sites, use CloudFront + S3:

import { defineStaticSite } from "effortless-aws";
export const site = defineStaticSite({
dir: "dist",
build: "npm run build",
spa: true,
});

Secrets and configuration

Pull secrets from SSM Parameter Store at cold start — cached, typed, auto-permissioned.

import { defineApi, param } from "effortless-aws";
export const checkout = defineApi({
basePath: "/checkout",
config: {
stripeKey: param("stripe/secret-key"),
webhookSecret: param("stripe/webhook-secret"),
},
post: async ({ config }) => {
// config.stripeKey is fetched from SSM once, cached across invocations
const stripe = new Stripe(config.stripeKey);
// ...
},
});

No manual SSM calls. No GetParameter permission writing. No environment variable plumbing.

What you don’t need to learn

ConceptTraditionalEffortless
IAM policiesWrite JSON policies, attach to rolesAutomatic from deps and config
CloudFormation / CDKLearn constructs, stacks, synthesisNot used
Terraform / HCLLearn HCL, manage state, plan/applyNot used
State managementS3 backends, locking, drift detectionAWS tags — no state files
API Gateway configRoutes, integrations, stages, deploymentsDerived from basePath + route definitions
DynamoDB streamsEvent source mappings, batch config, failure handlingAdd onRecord to your table
Lambda LayersBuild, publish, version, attach to functionsAutomatic for node_modules

What Effortless is not

  • Not multi-cloud. AWS only. This focus is what makes deep integration possible.
  • Not a managed platform. Deploys to your AWS account. You own the resources.
  • Not a full IaC tool. Covers the serverless product stack (Lambda, DynamoDB, SQS, S3, SES, CloudFront). For VPCs, RDS, or ECS — use Terraform/CDK alongside.
  • Not zero-config. You still need effortless.config.ts for project name and region. But that’s one file, not five.

Next steps

  • Installation — install and deploy your first handler in 2 minutes
  • Definitions — all definition types and their options
  • Architecture — how static analysis, bundling, and deployment work
  • Comparisons — detailed comparisons with SST, Nitric, Serverless Framework, and others