Ship the whole product
API, database, queues, email, website — one project, one deploy, automatic IAM wiring.
The fastest way to ship a TypeScript app onto AWS.
Ship the whole product
API, database, queues, email, website — one project, one deploy, automatic IAM wiring.
Deploy in seconds
Direct AWS API calls. No CloudFormation, no state files. Full deploy in ~5-10 seconds.
Type safety everywhere
Define a table once — get typed clients, typed streams, and runtime validation automatically.
Serverless by default
Pay per use, scale to zero, multi-AZ redundancy. Nothing runs when nobody is using your product.
| Your product needs | Effortless handler | AWS resources created |
|---|---|---|
| REST API | defineApi | Lambda + Function URL + IAM |
| Database | defineTable | DynamoDB + optional stream Lambda |
| Background jobs | defineQueue | SQS FIFO + consumer Lambda |
| File storage | defineBucket | S3 + optional event Lambda |
| Transactional email | defineMailer | SES + DKIM identity |
| Website / SSR app | defineApp | CloudFront + Lambda + S3 |
| Static site / SPA | defineStaticSite | CloudFront + S3 |
All in the same project, all deployed with one command, all with automatic IAM wiring between them.
All definitionsA typical AWS app splits this into two layers: an infrastructure description (CDK, Terraform, Pulumi…) and a separate handler using the raw AWS SDK. Effortless collapses both into one file.
One file. Table and API live together. IAM, table name, and Function URL are derived from .deps() automatically.
import { defineTable, defineApi } from "effortless-aws";
export const todos = defineTable<{ tag: "todo"; title: string }>().build();
export const api = defineApi({ basePath: "/todos" }) .deps(() => ({ todos })) .setup(({ deps }) => ({ todos: deps.todos })) .get({ path: "/" }, async ({ todos }) => ({ status: 200, body: await todos.query({ pk: "TODO" }), }));$ eff deploy # ~6s, direct AWS API callsAn IaC layer that describes the table, Lambda, IAM, and Function URL — plus a separate handler using the raw DynamoDB SDK. IAM, env vars, and runtime wiring are all manual. (Example shown in AWS CDK; the shape is similar with Terraform or Pulumi.)
import * as cdk from "aws-cdk-lib";import * as dynamodb from "aws-cdk-lib/aws-dynamodb";import * as lambda from "aws-cdk-lib/aws-lambda";import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs";
export class TodoStack extends cdk.Stack { constructor(scope: cdk.App, id: string) { super(scope, id);
const table = new dynamodb.Table(this, "Todos", { partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING }, sortKey: { name: "sk", type: dynamodb.AttributeType.STRING }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, });
const fn = new nodejs.NodejsFunction(this, "Api", { entry: "src/handler.ts", runtime: lambda.Runtime.NODEJS_20_X, environment: { TABLE_NAME: table.tableName }, });
table.grantReadData(fn); fn.addFunctionUrl({ authType: lambda.FunctionUrlAuthType.NONE }); }}import { DynamoDBClient, QueryCommand } from "@aws-sdk/client-dynamodb";import { unmarshall } from "@aws-sdk/util-dynamodb";
const client = new DynamoDBClient({});const TABLE = process.env.TABLE_NAME!;
export const handler = async () => { const res = await client.send(new QueryCommand({ TableName: TABLE, KeyConditionExpression: "pk = :pk", ExpressionAttributeValues: { ":pk": { S: "TODO" } }, })); return { statusCode: 200, body: JSON.stringify((res.Items ?? []).map(unmarshall)), };};$ cdk bootstrap # one-time$ cdk deploy # 2–5 min via CloudFormationSame logical operations. Effortless gives you a typed table client, derives IAM from code, and skips the IaC layer entirely.
Two files, two handlers. db.ts becomes a DynamoDB table with a stream Lambda; api.ts becomes one Lambda behind a Function URL serving all the routes.
This 9-line file becomes a DynamoDB table, a stream Lambda that runs on every change, and the IAM role linking them. The typed client (todos.query, todos.put, todos.update) is derived from Todo.
import { defineTable } from "effortless-aws";
export type Todo = { tag: "todo"; id: string; title: string; done: boolean };
export const todos = defineTable<Todo>() .onRecord(async ({ record }) => { if (record.eventName === "MODIFY" && record.new?.data.done) { console.log(`Todo completed: ${record.new.data.title}`); } });All routes run in one Lambda behind a single Function URL. .deps() lists handlers this API uses — Effortless wires IAM and injects a typed todos client through .setup().
import { defineApi } from "effortless-aws";import { z } from "zod";import { todos } from "./db";
const CreateTodo = z.object({ title: z.string() });const UpdateTodo = z.object({ done: z.boolean() });
export const todoApi = defineApi({ basePath: "/todos" }) .deps(() => ({ todos })) .setup(({ deps }) => ({ todos: deps.todos })) .get({ path: "/" }, async ({ todos }) => ({ status: 200, body: await todos.query({ pk: "TODO" }), })) .get({ path: "/{id}" }, async ({ req, todos }) => { const todo = await todos.get({ pk: "TODO", sk: req.params.id }); if (!todo) return { status: 404, body: { error: "Not found" } }; return { status: 200, body: todo.data }; }) .post({ path: "/", input: CreateTodo }, async ({ input, todos }) => { const id = crypto.randomUUID(); await todos.put({ pk: "TODO", sk: id, data: { tag: "todo", id, title: input.title, done: false }, }); return { status: 201, body: { id } }; }) .patch({ path: "/{id}", input: UpdateTodo }, async ({ req, input, todos }) => { await todos.update( { pk: "TODO", sk: req.params.id }, { set: { done: input.done } }, ); return { status: 200, body: { id: req.params.id, done: input.done } }; });$ eff deploy
Deployed 2 handler(s) in 6s: [table] todos arn:aws:dynamodb:eu-west-1:***:table/todo-app-dev-todos [api] todoApi https://abc123.lambda-url.eu-west-1.on.aws/todosNeed a separate environment? Add --stage:
$ eff deploy --stage prod
Deployed 2 handler(s) in 6s: [table] todos arn:aws:dynamodb:eu-west-1:***:table/todo-app-prod-todos [api] todoApi https://xyz789.lambda-url.eu-west-1.on.aws/todosFully isolated infrastructure — separate tables, Lambdas, Function URLs. No shared state between stages.
See how Effortless compares to SST, Nitric, Serverless Framework, and others — detailed comparisons.