Ship the whole product
API, database, queues, email, website — one project, one deploy, automatic IAM wiring.
Export handlers, deploy to AWS. No infrastructure files needed.
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 | defineFifoQueue | 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 definitionsEach tab adds a layer — every export creates real AWS infrastructure.
Type and schema — shared across handlers.
import { z } from "zod";
export type Todo = { id: string; title: string; done: boolean };
export const Command = z.discriminatedUnion("action", [ z.object({ action: z.literal("create"), title: z.string() }), z.object({ action: z.literal("done"), id: z.string(), done: z.boolean() }),]);One export — one API. Chained .get() routes handle queries, .post() handles commands.
import { defineApi } from "effortless-aws";import { todos } from "./db";import { Command } from "./todo";
export const todoApi = defineApi({ basePath: "/todos", deps: () => ({ todos }),}) .setup(({ deps }) => ({ todos: deps.todos })) .get("/", async ({ todos }) => ({ status: 200, body: await todos.query({ pk: "TODO" }), })) .get("/{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("/", async ({ input, todos }) => { const data = Command.parse(input); if (data.action === "create") { const id = crypto.randomUUID(); await todos.put({ pk: "TODO", sk: id, data: { id, title: data.title, done: false }, }); return { status: 201, body: { id } }; } await todos.update( { pk: "TODO", sk: data.id }, { set: { done: data.done } }, ); return { status: 200, body: { id: data.id, done: data.done } }; });Define a table — get a typed client. onRecord fires a stream Lambda on every change. config reads secrets from SSM at cold start.
import { defineTable, typed, param } from "effortless-aws";import type { Todo } from "./todo";
export const todos = defineTable({ schema: typed<Todo>(), config: { slackUrl: param("slack/webhook") }, onRecord: async ({ record, config }) => { if (record.eventName === "MODIFY" && record.new?.data.done) { await fetch(config.slackUrl, { method: "POST", body: JSON.stringify({ text: `Done: ${record.new.data.title}` }), }); } },});$ 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.