---
name: azion-implement-file-upload-with-functions
description: >-
  Build a Hono endpoint that checks an uploaded file and writes it to an Object Storage bucket from a function.
---

# Implement file upload with Functions

In this tutorial, you will build an upload endpoint that stores each file it receives in an [Object Storage](/en/documentation/platform/object-storage/) bucket. You will create a Hono project, write the upload handler, create a bucket with write access, deploy the project, and post a file to it.

The complete source of this example is in the [file-upload package](https://github.com/egermano/edge-functions-examples/tree/main/packages/file-upload) of the functions examples repository.

---

## Prerequisites

- An Azion account. To create one, refer to [How to create an account on Azion](/en/documentation/fundamentals/creating-account/).
- Azion CLI installed. Refer to [Azion CLI](/en/documentation/devtools/cli/).
- Object Storage enabled on your account. Refer to [Object Storage](/en/documentation/platform/object-storage/).
- [Node.js](https://nodejs.org/) version 18 or higher.
- A terminal and a code editor.

---

## 1. Create the project

Azion CLI creates a project from a framework preset. For the full Hono walkthrough, refer to [How to build with Hono](/en/documentation/guides/application-development/frameworks/hono/).

To create the project:

1. **Authenticate Azion CLI**

   Run the login command:

   ```bash
   azion login
   ```

   With no credential flags, the CLI opens a browser-based flow. It stores the credentials locally, and every later command is authorized against your account.

2. **Initialize the project**

   Run Azion CLI in the directory that holds your projects:

   ```bash
   azion init
   ```

3. **Name the project**

   Enter a name, or press `enter` to accept the suggestion:

   ```sh
   ? Your application's name:  (black-thor)
   ```

4. **Select the Hono preset**

   Azion CLI lists one preset per framework:

   ```sh
   ? Choose a preset:  [Use arrows to move, type to filter]
     Angular
     Astro
     Docusaurus
     Eleventy
     Emscripten
     Gatsby
     Hexo
   > Hono
     Hugo
     Javascript
     ...
   ```

5. **Select one of the available templates**

6. **Answer N to the local development server prompt**

   The remaining stages deploy the project instead of running it locally:

   ```sh
   Do you want to start a local development server? (y/N)
   ```

7. **Go to the project folder**

   Azion CLI names the folder after the project:

   ```bash
   cd <your-project-name>
   ```

The folder holds the source code of the Hono application that the template supplies.

---

## 2. Write the upload handler

The handler answers a `POST` request on `/upload`. It parses the multipart form, checks the file, and writes it to a bucket. The write goes through the `createObject` method of the [Azion Storage library](/en/documentation/devtools/azion-lib/storage/).

The `entry` field of `azion.config.js` names the entry file of the project. Open that file and replace its contents with this code:

```typescript
import type { AzionBucketObject, AzionStorageResponse } from "azion/storage";
import { createObject } from "azion/storage";
import { Hono } from "hono";

const app = new Hono();

app.post("/upload", async (c) => {
  const body = await c.req.parseBody();
  const file = body["file"];

  if (
    !file ||
    typeof file !== "object" ||
    typeof file.arrayBuffer !== "function"
  ) {
    return c.json({ message: "Invalid file" }, 400);
  }

  const maxSize = 2 * 1024 * 1024; // 2 MB, in bytes.
  if (file.size > maxSize) {
    return c.json({ message: "File size exceeds 2MB limit" }, 413);
  }

  try {
    const { data: newObject, error }: AzionStorageResponse<AzionBucketObject> =
      await createObject({
        bucket: Azion.env.get("BUCKET_NAME")!,
        key: file.name,
        // @ts-expect-error content is wrongly typed
        content: await file.arrayBuffer(),
      });

    if (error) {
      throw new Error(error.message);
    }

    if (newObject) {
      console.log(`Object created with key: ${newObject.key}`);
    } else {
      console.error("Failed to create object", error);
    }

    // @ts-expect-error content is wrongly typed
    const content = new Uint8Array(newObject.content);

    return c.body(content, {
      status: 200,
      headers: {
        "Content-Type": file.type,
        "Content-Disposition": `attachment; filename="${newObject?.key}"`,
        "Content-Length": newObject?.size?.toString() ?? "0",
      },
    });
  } catch (error) {
    console.error("Error uploading file:", error);
    return c.json({ message: "Error uploading file" }, 500);
  }
});

export default app;
```

`export default app` is the ES Modules handler pattern, which Azion recommends over the Service Worker pattern. Four decisions sit in the code:

- A `file` field that holds no file returns `400` with the body `{"message":"Invalid file"}`.
- A file above the `maxSize` constant of 2 MB returns `413` with the body `{"message":"File size exceeds 2MB limit"}`.
- `createObject` writes the file to the bucket named by the `BUCKET_NAME` environment variable. The object key is the name of the file.
- A failure inside `createObject` returns `500` with the body `{"message":"Error uploading file"}`, and the exception reaches the function logs.

> **Caution**
>
> The sample builds the object key from `file.name`, a value the request supplies. An upload that reuses an existing object key replaces the object, and the earlier version cannot be retrieved. Build the key from a value your application controls, so one upload does not replace another. Strip path separators and control characters from any request value that reaches a key. The request also supplies `file.type`, so check it against the types the endpoint accepts.

---

## 3. Configure storage permissions

The access level of a bucket decides what Azion Runtime does with it. A function writes objects only to a bucket whose access is `read_write`. A bucket set to `read_only` answers reads and rejects writes, and Azion Runtime reaches no content in a bucket set to `restricted`.

> **Caution**
>
> Any user can modify the content of a bucket set to `read_write`. When a function reaches the bucket, the code of that function decides what reaches storage. Keep the validation in the handler.

A bucket name is unique across all Azion accounts. It takes 6 to 63 characters, accepts letters, numbers, and the hyphen (`-`), and never starts with `azion`.

The handler reads the bucket name from the `BUCKET_NAME` environment variable. To create the bucket and store its name:

1. **Create the bucket the handler writes to**

   ```bash
   azion create storage bucket --name "<your-bucket-name>" --workloads-access 'read_write'
   ```

   The bucket exists on your account, and Azion Runtime writes objects to it.

2. **Store the bucket name on your account**

   Enter the same name you gave the bucket:

   ```bash
   azion create variables --key "BUCKET_NAME" --value "<your-bucket-name>" --secret false
   ```

The variable is stored on the account, and the function reads it with `Azion.env.get('BUCKET_NAME')`. A variable whose key contains `password`, `pwd`, `secret`, `key`, `hash`, `encrypted`, `passcode`, `auth`, or `token` is sent as a secret by default. The key `BUCKET_NAME` carries none of those substrings. The `--secret` flag defaults to `true`, so `--secret false` stores the bucket name as a plain value. A new value reaches a function only after the function is deployed again.

---

## 4. Deploy the project

To send the project to Azion:

1. **Confirm which account Azion CLI uses**

   Print the authenticated account:

   ```bash
   azion whoami
   ```

   The terminal prints the email address of the account the commands run against.

2. **Deploy the project**

   In the project folder, run:

   ```bash
   azion deploy
   ```

The deploy uploads the function code and configures the application. It also creates the routing rules, applies the storage permissions, and returns a domain. The domain has the format `https://xxxxxxx.map.azionedge.net`. Propagation takes a few minutes, so wait before you request the endpoint.

> **Note**
>
> Azion CLI opens the browser on the page of Azion Console that carries the deployment logs. When the browser does not open, use the link the terminal prints.

---

## 5. Verify the upload

To store a file through the endpoint:

1. **Create a text file**

   Write one line into a local file:

   ```bash
   echo "Azion file upload test" > upload-test.txt
   ```

   The file `upload-test.txt` exists in the current directory.

2. **Post the file to the upload route**

   Send the file in a multipart form, under the `file` field:

   ```bash
   curl -F "file=@upload-test.txt" https://<your-azion-domain>/upload
   ```

   The response body carries the stored object:

   ```text
   Azion file upload test
   ```

3. **List the objects of the bucket**

   Read the keys the bucket holds:

   ```bash
   azion list storage object --bucket-name "<your-bucket-name>"
   ```

   The key `upload-test.txt` appears in the list.

The endpoint now stores every file it accepts. A request whose `file` field holds no file answers `400`, and a file above 2 MB answers `413`. When a request answers `500`, read the function logs for the exception behind it.

---

## Next steps

- [Object Storage](/en/documentation/platform/object-storage.md): Bucket permissions, object keys, prefixes, and the operation classes that are billed.
- [Azion Storage library](/en/documentation/devtools/azion-lib/storage.md): Every method of azion/storage, including getObjectByKey, updateObject, and deleteObject.
- [Environment variables](/en/documentation/platform/functions/environment-variables.md): Store configuration and secrets outside the function code, then read each one by key at run time.
- [Troubleshoot function execution and logs](/en/documentation/platform/functions/troubleshooting.md): The fixes for a function that never runs, stops before it responds, or writes nothing to the logs.
- [Functions best practices](/en/documentation/platform/functions/best-practices.md): What each part of the handler does, and the design choices that keep an invocation inside its limits.
- [Limits](/en/documentation/platform/functions/limits.md): The body size a function processes per plan, and every other Functions ceiling.
