---
name: s3-express
description: "Provisions an S3 bucket with keys scoped to it for a Express app with `npx extraorbital add s3` (no signup, no card) and wires it into Express with `@aws-sdk/client-s3`. Use when a Express project needs S3 storage, object storage, file uploads, a bucket, blob storage, R2, B2 or S3_BUCKET, or when S3_BUCKET is missing or unset in .env."
---

# S3 storage in Express

<!-- Generated from ExtraOrbital's service registry by `pnpm skills:build` in platform/app. Edit the generator, not this file. -->

ExtraOrbital provisions an S3 bucket with keys scoped to it in one command and writes the credentials into the project's env file. For anything this recipe does not cover, use the `extraorbital` skill or https://extraorbital.dev/llms.txt.

## 1. Provision

```bash
npx extraorbital add s3
```

Run it from the project root. It is idempotent (a second run returns the same resource) and writes to `.env.local` if the project has one, otherwise `.env`, keeping that file out of git. Add `--json` to parse the result; credential values are redacted there, and you should never print them either.

| Variable | |
| --- | --- |
| `S3_BUCKET` |  |
| `S3_ENDPOINT` |  |
| `S3_REGION` |  |
| `S3_ACCESS_KEY_ID` |  |
| `S3_SECRET_ACCESS_KEY` |  |

Free on the Prototype plan: 1 GB, 10K simple + 2K advanced ops/mo.

## 2. Install the client

```bash
npm install @aws-sdk/client-s3
```

## 3. Connect

Node does not read env files by itself: start with `node --env-file=.env` (Node 20.6+), naming `.env.local` instead if that is where ExtraOrbital wrote, or load it with `dotenv`.

**`src/storage.js`**

```js
import { S3Client } from "@aws-sdk/client-s3";

export const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region: process.env.S3_REGION,
  // Path-style addressing works on AWS and on S3-compatible endpoints alike.
  forcePathStyle: true,
  credentials: {
    accessKeyId: process.env.S3_ACCESS_KEY_ID,
    secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
  },
});
export const s3Bucket = process.env.S3_BUCKET;
```

## 4. Use it

**`src/app.js`**

```js
import { ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
import { s3, s3Bucket } from "./storage.js";

// Express 5 sends a rejected promise to the error handler; on Express 4, wrap this in try/catch.
app.get("/api/s3", async (req, res) => {
  await s3.send(new PutObjectCommand({ Bucket: s3Bucket, Key: "hello.txt", Body: "hi" }));
  const listed = await s3.send(new ListObjectsV2Command({ Bucket: s3Bucket, MaxKeys: 10 }));
  res.json({ keys: listed.Contents?.map((o) => o.Key) ?? [] });
});
```

## 5. Verify

```bash
npx extraorbital list        # the resource is listed as active
node --env-file=.env src/server.js
curl http://localhost:3000/api/s3
```

A JSON answer means the credentials, the client and the route all work. Delete the route afterwards if the app does not need it.

## S3-compatible alternatives

Same client, same code; only the variable names differ. Pick one when the project has a reason to:

- `npx extraorbital add r2` — Cloudflare R2: no egress fees; region `auto`; on the free plan files expire 30 days after upload. Writes `R2_BUCKET`, `R2_ENDPOINT`, `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`.
- `npx extraorbital add b2` — Backblaze B2: cheaper per stored byte. Writes `B2_BUCKET`, `B2_ENDPOINT`, `B2_REGION`, `B2_ACCESS_KEY_ID`, `B2_SECRET_ACCESS_KEY`.

## Pitfalls

- Objects are private unless the bucket was created with `--public` (`npx extraorbital add s3 --public`); options apply at creation only.
- For browser uploads, hand out a presigned URL (`@aws-sdk/s3-request-presigner`) rather than the keys.
- The keys reach this bucket only: listing buckets or creating new ones is refused.
- Snippets are ES modules (`"type": "module"`). In CommonJS, swap `import` for `require`.
- Create clients once at module scope, as here, not per request.
- Exit code 3 means the account is past its free allowance: show the human the link the command printed and wait. Do not retry or work around it.

Docs: https://extraorbital.dev/docs/resources/s3.md
