---
name: azion-build-a-screenshot-api-with-functions-and-browserless
description: >-
  Deploy a Hono project on Functions that captures a page screenshot through the Browserless API and returns the PNG image.
---

# Build a screenshot API with Functions and Browserless

In this tutorial, you will build a screenshot API that returns a web page as a PNG image. You will create the project from the Hono template, write the route, store the API token, deploy the project, and request the endpoint.

The [function](/en/documentation/platform/functions/) runs no browser of its own. It posts the target URL to [Browserless](https://browserless.io/), a hosted service that renders the page and answers with the image. The function returns those bytes to the client.

---

## Prerequisites

- An Azion account. To create one, refer to [How to create an account on Azion](/en/documentation/fundamentals/creating-account/).
- Azion CLI installed on your machine. Refer to [Azion CLI](/en/documentation/devtools/cli/).
- Node.js version 18 or higher.
- A Browserless account and its API token. To create one, refer to [Browserless](https://browserless.io/).
- Working knowledge of JavaScript.

---

## 1. Authenticate Azion CLI

Sign in to your Azion account from the terminal:

```bash
azion login
```

Azion CLI opens a browser-based flow when you omit the credential flags. It stores the resulting credentials locally, and they authorize every later command.

---

## 2. Create the project from the Hono template

Azion CLI initializes the project from a starter template. To create it:

1. **Initialize the project**

   Run the command and answer the prompts that follow:

   ```bash
   azion init --name screenshot-api
   ```

2. **Select Hono in the preset list**

   The prompt 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
     ...
   ```

3. **Select the Hono Boilerplate template**

4. **Enter n at the prompt for a local development server**

   The prompt appears after Azion CLI fetches and configures the template:

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

5. **Enter n at the deploy prompt**

   The route and the API token are not in place yet:

   ```sh
   Do you want to deploy your project? (y/N)
   ```

Azion CLI creates the `screenshot-api` directory and writes the Hono project into it. Go to that directory:

```bash
cd screenshot-api
```

The remaining commands run from there.

---

## 3. Write the screenshot route

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

```javascript
import { Hono } from 'hono';

const app = new Hono();

app.get('/screenshot', async (c) => {
  const url = c.req.query('url') || 'https://www.example.com';
  const token = Azion.env.get('PUPPETEER_BROWSERLESS_IO_KEY');

  const response = await fetch(
    `https://production-sfo.browserless.io/screenshot?token=${token}`,
    {
      method: 'POST',
      headers: {
        'Cache-Control': 'no-cache',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        url,
        options: { type: 'png' },
      }),
    },
  );

  if (!response.ok) {
    return c.html(await response.text(), response.status);
  }

  const image = new Uint8Array(await response.arrayBuffer());

  return c.body(image, {
    status: 200,
    headers: { 'Content-Type': 'image/png' },
  });
});

export default app;
```

The route reads the target page from the `url` query parameter, and it falls back to `https://www.example.com`. `export default app` is the ES Modules handler pattern, which Azion recommends over the Service Worker pattern. To compare both patterns, refer to [Migrate handler patterns in Functions](/en/documentation/guides/application-development/functions-and-runtime/migrate-handler-patterns/).

The complete project is in the [browserless package of the functions examples repository](https://github.com/egermano/edge-functions-examples/tree/main/packages/browserless).

---

## 4. Store the Browserless token as an environment variable

An environment variable holds the token outside the function code and outside version control. To create it:

```bash
azion create variables --key "PUPPETEER_BROWSERLESS_IO_KEY" --value "[TOKEN VALUE]" --secret true
```

Azion stores the variable on the account, and `Azion.env.get()` returns its value at run time. A variable whose key contains `password`, `pwd`, `secret`, `key`, `hash`, `encrypted`, `passcode`, `auth`, or `token` is sent as a secret by default. The `--secret true` flag repeats that default here.

A change to a variable does not reach a function that is already running. Redeploy the function for a new value to take effect. Azion Runtime also reads a variable through `process.env`. For both interfaces, the fields of a variable, and the other management interfaces, refer to [Environment variables](/en/documentation/platform/functions/environment-variables/).

---

## 5. Deploy the project

Build the project and send it to Azion:

```bash
azion deploy
```

The command uploads the function code and configures the application and its routing rules. It returns a workload domain in the format `https://xxxxxxx.map.azionedge.net`.

> **Note**
>
> Azion opens a page in [Azion Console](https://console.azion.com/) that shows the deployment logs. Open the printed link when the browser does not open on its own.

The application answers on that domain a few minutes later, once the DNS propagation completes.

---

## 6. Verify the screenshot response

Request the route with the address of the page to capture:

```bash
curl -s -o screenshot.png -w '%{content_type}\n' "https://<your-azion-domain>/screenshot?url=https://www.example.com"
```

The command prints the content type of the response:

```
image/png
```

The file `screenshot.png` holds the capture of `https://www.example.com`. A request without the `url` parameter captures the same page, because the route falls back to it. When Browserless rejects a request, the route answers with the body and the status code that Browserless returned.

---

## Next steps

- [Environment variables](/en/documentation/platform/functions/environment-variables.md): The fields of a variable, the size ceilings, and every interface that creates one.
- [Migrate handler patterns in Functions](/en/documentation/guides/application-development/functions-and-runtime/migrate-handler-patterns.md): The ES Modules and Service Worker handlers side by side, with the parameters each one receives.
- [Limits](/en/documentation/platform/functions/limits.md): The CPU time, execution time, sub-request, and memory ceilings a single invocation runs inside.
- [Troubleshoot function execution and logs](/en/documentation/platform/functions/troubleshooting.md): Read the function logs, and find the cause when a function never runs or stops before it responds.
- [Object Storage](/en/documentation/platform/object-storage.md): Buckets on the Azion Web Platform, reachable through the S3 standard, to keep the images the route captures.
- [How to build with Hono](/en/documentation/guides/application-development/frameworks/hono.md): The rest of the Hono workflow on Azion, including the local development server.
