> ## Documentation Index
> Fetch the complete documentation index at: https://wemstudios.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Storing and Serving Files with Cloudflare R2 in Blaze

> Upload, retrieve, and delete objects in Cloudflare R2 directly from Blaze route handlers using req.env.R2. Includes streaming, metadata, and ETag support.

Cloudflare R2 is S3-compatible object storage with zero egress fees, and Blaze puts it directly on `req.env.R2` in every handler. Whether you need to accept file uploads, serve assets, or build a private document store, R2 fits naturally into any Blaze route — no SDK to initialize, no credentials to manage beyond your `wrangler.toml` binding.

## Configure the binding

<Steps>
  <Step title="Add the bucket to wrangler.toml">
    ```toml wrangler.toml theme={null}
    [[r2_buckets]]
    binding     = "R2"
    bucket_name = "my-app-assets"
    ```
  </Step>

  <Step title="Define the Env type">
    ```typescript src/types/env.ts theme={null}
    export type Env = {
      R2: R2Bucket;
      // ...other bindings
    };
    ```
  </Step>

  <Step title="Pass Env to createApp">
    ```typescript src/index.ts theme={null}
    import { createApp } from "blaze";
    import type { Env } from "./types/env";

    const app = createApp<Env>();
    // req.env.R2 is now fully typed as R2Bucket in every handler
    ```
  </Step>
</Steps>

## Uploading a file

Read the raw request body with `req.arrayBuffer()`, then call `req.env.R2.put(key, body, options)`. Pass `httpMetadata` to preserve the content type and `customMetadata` to store any application-level attributes alongside the object.

```typescript theme={null}
app.put("/files/:key", async (req, res) => {
  const body        = await req.arrayBuffer();
  const contentType = req.header("Content-Type") ?? "application/octet-stream";

  await req.env.R2.put(req.params.key, body, {
    httpMetadata: {
      contentType,
    },
    customMetadata: {
      uploadedBy:  req.user?.id ?? "anonymous",
      uploadedAt:  new Date().toISOString(),
    },
  });

  res.status(201).json({ key: req.params.key });
});
```

<Note>
  R2 objects are limited to **5 TB** per object. For uploads larger than a few
  hundred MB, consider using R2's multipart upload API via a presigned URL so
  the client streams directly to R2 instead of buffering through your Worker.
</Note>

## Downloading a file

`req.env.R2.get(key)` returns an `R2ObjectBody` when the object exists or `null` when it does not. Always check for `null` before accessing the body, then forward the content type and ETag headers before streaming the response.

```typescript theme={null}
app.get("/files/:key", async (req, res) => {
  const obj = await req.env.R2.get(req.params.key);

  if (!obj) {
    return res.status(404).json({ error: "Object not found" });
  }

  res.header(
    "Content-Type",
    obj.httpMetadata?.contentType ?? "application/octet-stream"
  );
  res.header("ETag", obj.httpEtag);

  res.send(await obj.arrayBuffer());
});
```

## Listing objects

`req.env.R2.list()` returns the first page of objects in the bucket. Use the `prefix` option to scope results to a virtual directory and `cursor` to paginate through large buckets.

```typescript theme={null}
app.get("/files", async (req, res) => {
  const prefix = req.query.get("prefix") ?? undefined;
  const cursor = req.query.get("cursor")  ?? undefined;

  const listing = await req.env.R2.list({
    prefix,
    cursor,
    limit: 100,
  });

  res.json({
    objects: listing.objects.map((o) => ({
      key:          o.key,
      size:         o.size,
      etag:         o.etag,
      lastModified: o.uploaded,
    })),
    truncated: listing.truncated,
    cursor:    listing.truncated ? listing.cursor : undefined,
  });
});
```

## Deleting objects

`req.env.R2.delete(key)` removes a single object. Deleting a key that does not exist is a no-op — R2 returns success regardless.

```typescript theme={null}
app.delete("/files/:key", async (req, res) => {
  await req.env.R2.delete(req.params.key);
  res.status(204).send();
});
```

## Metadata and ETags

Every R2 object exposes `httpMetadata` (standard HTTP headers stored at upload time) and `httpEtag` (a content fingerprint). Use these to power proper browser caching without any extra computation in your Worker.

```typescript theme={null}
app.get("/files/:key/meta", async (req, res) => {
  // R2.head() fetches metadata without downloading the body
  const obj = await req.env.R2.head(req.params.key);

  if (!obj) {
    return res.status(404).json({ error: "Object not found" });
  }

  res.json({
    key:          obj.key,
    size:         obj.size,
    etag:         obj.httpEtag,
    contentType:  obj.httpMetadata?.contentType,
    customMeta:   obj.customMetadata,
    lastModified: obj.uploaded,
  });
});
```

When serving files to browsers, set the `ETag` header and let Blaze's `etag` middleware handle `If-None-Match` negotiation automatically, returning a `304 Not Modified` when the client already holds a fresh copy:

```typescript theme={null}
import { etag } from "blaze/middleware/etag";

app.get("/files/:key", etag(), async (req, res) => {
  const obj = await req.env.R2.get(req.params.key);
  if (!obj) return res.status(404).json({ error: "Not found" });

  res.header("Content-Type",  obj.httpMetadata?.contentType ?? "application/octet-stream");
  res.header("ETag",          obj.httpEtag);
  res.header("Cache-Control", "public, max-age=31536000, immutable");

  res.send(await obj.arrayBuffer());
});
```

<Tip>
  Use Blaze's built-in `etag` middleware alongside R2's native `httpEtag` for
  **conditional GET support** out of the box. The middleware intercepts
  `If-None-Match` request headers and short-circuits the handler with a `304`
  response when the ETag matches — saving both bandwidth and R2 read costs on
  frequently accessed objects.
</Tip>
