1---
2title: DynamoDB
3description: Add a DynamoDB Table, bind GetItem and PutItem to your Lambda, and serve a typed key/value HTTP API backed by DynamoDB.
4---
5
6**DynamoDB** is AWS's serverless key/value store — structured
7records looked up by partition (and optional sort) key, with no
8capacity to manage. In Alchemy it's the `Table` resource plus a
9binding per operation: `GetItem`, `PutItem`, `UpdateItem`,
10`DeleteItem`, `Query`, `Scan`, and the batch/transact variants.
11Each binding attaches a least-privilege IAM statement scoped to
12the table's ARN. Tables are also an event source: enable
13**DynamoDB Streams** and consume change records in a Lambda with
14`DynamoDB.consumeTableChanges` (see the
15[DynamoDB Streams guide](/aws/messaging/dynamodb-streams)).
16
17The walkthrough below builds on the [Lambda](/aws/compute/lambda) and
18[S3](/aws/data/s3) pages: it adds a **Table** and binds `GetItem` +
19`PutItem` to the function — same pattern as S3, different
20storage.
21
22## Define the table
23
24DynamoDB tables need a partition key (and optionally a sort key)
25declared up-front. Yield the `Table` resource in the function's
26constructor, mirroring how you added the bucket:
27
28```diff lang="typescript"
29// src/api.ts
30import * as AWS from "alchemy/AWS";
31+import * as DynamoDB from "alchemy/AWS/DynamoDB";
32import * as S3 from "alchemy/AWS/S3";
33import { Stack } from "alchemy/Stack";
34import * as Effect from "effect/Effect";
35// ...
36
37Effect.gen(function* () {
38 const bucket = yield* S3.Bucket("Blobs");
39+ const table = yield* DynamoDB.Table("Items", {
40+ partitionKey: "id",
41+ attributes: {
42+ id: "S",
43+ },
44+ });
45
46 // ... existing bindings + handler ...
47});
48```
49
50`attributes` only needs to list the attributes used by keys (and
51secondary indexes) — DynamoDB itself is schemaless on the rest of
52the row. `"S"` is the AWS shorthand for "string"; `"N"` is number
53and `"B"` is binary.
54
55## Bind GetItem and PutItem
56
57Same shape as the S3 bindings — `PutItem(table)` returns a callable
58Effect and quietly attaches a least-privilege IAM policy scoped to
59this table's ARN:
60
61```diff lang="typescript"
62Effect.gen(function* () {
63 const bucket = yield* S3.Bucket("Blobs");
64 const table = yield* DynamoDB.Table("Items", { /* ... */ });
65
66 const putObject = yield* S3.PutObject(bucket);
67 const getObject = yield* S3.GetObject(bucket);
68+ const putItem = yield* DynamoDB.PutItem(table);
69+ const getItem = yield* DynamoDB.GetItem(table);
70
71 return { fetch: /* ... */ };
72});
73```
74
75## Write items with `PUT /items/:id`
76
77Add a branch to `fetch` that writes a row keyed by the URL
78segment. DynamoDB takes attribute values in their native typed
79form — `{ S: "..." }` for strings, `{ N: "42" }` for numbers —
80and Alchemy bindings deliberately **don't** auto-marshal so you
81stay on the raw SDK shape:
82
83```diff lang="typescript"
84fetch: Effect.gen(function* () {
85 const request = yield* HttpServerRequest;
86 const url = new URL(request.url);
87 const path = url.pathname;
88
89+ if (path.startsWith("/items/") && request.method === "PUT") {
90+ const id = path.slice("/items/".length);
91+ const content = yield* request.text;
92+ yield* putItem({
93+ Item: {
94+ id: { S: id },
95+ content: { S: content },
96+ },
97+ });
98+ return HttpServerResponse.empty({ status: 204 });
99+ }
100
101 // ... existing /:key blob routes ...
102}).pipe(Effect.orDie),
103```
104
105Skipping marshalling keeps the binding tiny and avoids a
106debate over which marshalling layer to use. If you want a
107typed-object API across your codebase, build it as a thin
108wrapper around `putItem` — the binding won't fight you.
109
110## Read items with `GET /items/:id`
111
112The read path uses `getItem` with the same `{ S: id }` key
113shape:
114
115```diff lang="typescript"
116if (path.startsWith("/items/") && request.method === "PUT") {
117 // ... unchanged ...
118}
119
120+if (path.startsWith("/items/") && request.method === "GET") {
121+ const id = path.slice("/items/".length);
122+ const result = yield* getItem({ Key: { id: { S: id } } });
123+ if (!result.Item) {
124+ return HttpServerResponse.text("Not found", { status: 404 });
125+ }
126+ return yield* HttpServerResponse.json({
127+ id: result.Item.id?.S,
128+ content: result.Item.content?.S,
129+ });
130+}
131```
132
133`getItem` returns `result.Item` as `undefined` when the row
134doesn't exist — no need for a typed `catchTag` like S3's
135`NoSuchKey`. Map the AWS attribute-value shape back to a plain
136object at the response boundary.
137
138## Provide the runtime layers
139
140Add the DynamoDB lives next to the S3 ones:
141
142```diff lang="typescript"
143}).pipe(
144 Effect.provide(
145 Layer.mergeAll(
146 AWS.Lambda.BucketEventSource,
147+ DynamoDB.GetItemHttp,
148+ DynamoDB.PutItemHttp,
149 S3.PutObjectHttp,
150 S3.GetObjectHttp,
151 ),
152 ),
153),
154```
155
156## Deploy and verify
157
158```sh
159bun alchemy deploy
160```
161
162Add a round-trip test that exercises the new routes:
163
164```typescript
165// test/integ.test.ts
166test(
167 "DynamoDB round-trip",
168 Effect.gen(function* () {
169 const { url } = yield* stack;
170
171 yield* HttpClient.put(`${url}/items/abc`, {
172 body: HttpBody.text("hello dynamo"),
173 });
174
175 const get = yield* HttpClient.get(`${url}/items/abc`);
176 expect(yield* get.json).toEqual({ id: "abc", content: "hello dynamo" });
177 }),
178);
179```
180
181```sh
182bun test test/integ.test.ts
183```
184
185The IAM policy attached to your function role now includes
186`dynamodb:GetItem` and `dynamodb:PutItem` — both scoped to this
187specific table — alongside the S3 grants.
188
189## Where next
190
191- [Process DynamoDB Streams](/aws/messaging/dynamodb-streams) —
192 fan table changes out into a Lambda-side `Stream` you can
193 react to.
194- [SQS](/aws/messaging/sqs) — a general-purpose queue for background
195 work, fed from the same Lambda.
196- [`Table` reference](/providers/aws/dynamodb/table) — every
197 prop and attribute, including secondary indexes, TTL, and
198 point-in-time recovery.