# Browsers in Custom APIs - Playwright and Puppeteer

> Run a headless Chromium from API Maker custom APIs to make PDFs, take screenshots or read web pages - Playwright or Puppeteer on the native process, or Playwright inside the sandbox with its own Docker file.

Source: https://docs.apimaker.dev/v1/docs/guides/browser-automation.html

A custom API can drive a headless Chromium with [Playwright](https://playwright.dev/) or [Puppeteer](https://pptr.dev/) : turn HTML into a PDF, take a screenshot, or read a page. A browser needs system libraries next to its npm package, so it runs in one of two places.

| | Native process | Sandbox |
|---|---|---|
| Where the code runs | Inside API Maker itself (`runOnNativeProcess: true`) | In the sandbox containers of the account |
| Where the browser is installed | On the server, next to API Maker | In the Docker file of the sandbox |
| Isolation | None : a crash of the browser can hurt the server | Your code stays apart from API Maker |
| Set up by | The root user of the server | An admin, from the admin panel |

A browser needs memory : plan 2 GB of RAM and 40 GB of disk for the server, and avoid many browsers at the same time.

## Playwright on the native process

1. Sign in to the server as root (`whoami` prints `root` ; `sudo -s` otherwise) and install Chromium with the libraries it needs :

    ```bash
    npx playwright install --with-deps chromium
    ```

2. Add the package to API Maker :

    ```bash
    cd /root/projects/sava_api_maker/
    npm i playwright
    ```

3. Create a custom API and set `runOnNativeProcess: true` in its configuration (the **Basic Info** section of the [custom API](/v1/docs/apis-all/custom-apis/user-created-custom-api.html#native-process)).

4. Use it in the code. This one writes a PDF from HTML into the `uploads` folder :

```ts title="Custom API : HTML to PDF with Playwright"
import * as T from 'types';
import { join } from 'path';
const playwright = require('playwright');

async function main(g: T.IAMGlobal) {
    const browser = await playwright.chromium.launch();
    const page = await browser.newPage();
    await page.setContent('<p style="font: 18px Arial">Hello from Playwright</p>', { waitUntil: 'load' });
    await page.pdf({
        path: join(__dirname, 'uploads', 'playwright.pdf'),
        format: 'A4',
        printBackground: true,
        margin: { top: '20mm', right: '20mm', bottom: '20mm', left: '20mm' },
    });
    await browser.close();

    // uploads/playwright.pdf is on the server : upload it to a storage provider or send it as a download.
    return 'pdf created on the server';
}

module.exports = main;
```

!!! tip "Every server, every upgrade"
    A package installed by hand lives on one server. Put the install command in **Root Settings → Deployment Settings → Scripts → Server Startup Script** : the script runs when the API Maker process starts, on every server of the cluster, so the package is there on new servers too.

## Playwright in the sandbox

The default image of the sandbox has no browser libraries. Two changes in the [sandbox settings](/v1/docs/settings/sandboxSettings.html) add them.

1. **Docker file** : replace the content with the file below and click **Save** (top right). It installs Chromium, its system libraries and pnpm.

    ```dockerfile
    FROM node:22-bookworm
    WORKDIR /usr/src/app

    RUN npm install -g pnpm@10.27.0
    RUN apt-get update && apt-get install -y --no-install-recommends build-essential g++ make libc6 python3 python3-dev && rm -rf /var/lib/apt/lists/*

    ARG A_DOCKERFILE_HASH
    ENV A_DOCKERFILE_HASH=${A_DOCKERFILE_HASH}

    ARG A_PACKAGE_JSON_HASH
    ENV A_PACKAGE_JSON_HASH=${A_PACKAGE_JSON_HASH}
    COPY ./package.json ./
    RUN pnpm install --dangerously-allow-all-builds
    RUN npx -y playwright@1.58.0 install --with-deps

    ARG A_CODE_HASH
    ENV A_CODE_HASH=${A_CODE_HASH}

    ARG NODE_OPTIONS
    ENV NODE_OPTIONS=${NODE_OPTIONS}

    COPY . .

    ARG A_NEW_PACKAGES_INSTALL_CMD
    ENV A_NEW_PACKAGES_INSTALL_CMD=${A_NEW_PACKAGES_INSTALL_CMD}
    RUN $A_NEW_PACKAGES_INSTALL_CMD

    EXPOSE 4631
    EXPOSE 4632
    EXPOSE 9229
    CMD [ "npm", "run", "start" ]
    ```

2. **Dependencies** : click **Add (+)**, search for `playwright`, pick the latest stable version and click **Add**.

Launch Chromium with `--no-sandbox` inside the container. This custom API saves a web page as a PDF and sends it as a download :

```ts title="Custom API : a web page to PDF, in the sandbox"
import * as T from 'types';
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');

async function main(g: T.IAMGlobal) {
    const outDir = path.join(__dirname, 'uploads');
    if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });

    const browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] });
    const page = await browser.newPage();
    await page.goto('https://playwright.dev', { waitUntil: 'networkidle' });
    await page.pdf({ path: path.join(outDir, 'playwright-dev.pdf'), format: 'A4' });
    await browser.close();

    return {
        __am__downloadFilePath: 'playwright-dev.pdf',
        __am__downloadFolderFileName: 'playwright-dev.pdf',
    };
}

module.exports = main;
```

## Puppeteer on the native process

1. Sign in to the server as root and install the libraries of Chromium (Ubuntu ; for other systems see [the troubleshooting page of Puppeteer](https://pptr.dev/troubleshooting#chrome-doesnt-launch-on-linux)) :

    ```bash
    apt update && apt install -y ca-certificates fonts-liberation libatk1.0-0t64 libatk-bridge2.0-0t64 \
      libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
      libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 \
      libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \
      libxss1 libxtst6 libasound2t64 lsb-release wget xdg-utils
    ```

2. Add the package to API Maker :

    ```bash
    cd /root/projects/sava_api_maker/
    npm install puppeteer
    ```

3. Set `runOnNativeProcess: true` in the configuration of the custom API, and use it :

```ts title="Custom API : HTML to PDF with Puppeteer"
import * as T from 'types';
import { join } from 'path';
const puppeteer = require('puppeteer');

async function main(g: T.IAMGlobal) {
    const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });
    const page = await browser.newPage();
    await page.setContent('<p>Hello, this paragraph is converted to a PDF using Puppeteer.</p>', { waitUntil: 'load' });
    await page.pdf({ path: join(__dirname, 'uploads', 'puppeteer.pdf'), format: 'A4', printBackground: true });
    await browser.close();

    // uploads/puppeteer.pdf is on the server : upload it to a storage provider or send it as a download.
    return 'ok';
}

module.exports = main;
```

## Good to know

- Write the files you want to send into the `uploads` folder and return `__am__downloadFilePath` : see [Download file](/v1/examples/custom-apis/custom-api.html#download-file). Upload them to a storage service instead when they must be kept.
- Close the browser in every path of the code, errors included : an open browser keeps its memory.
- A bug on the native process can hurt the whole server : prefer the sandbox when you can, and keep the native process for the APIs which need it.

## Related

- [Custom APIs](/v1/docs/apis-all/custom-apis/user-created-custom-api.html) · [Sandbox settings](/v1/docs/settings/sandboxSettings.html) · [How API Maker works](/v1/docs/getting-started/how-it-works.html#where-your-code-runs)
- Blogs : [Playwright on the native process](/v1/docs/blog/General-Blogs/Playwright.html) · [Playwright in the sandbox](/v1/docs/blog/General-Blogs/Playwright-in-sandbox.html) · [Puppeteer in native custom APIs](/v1/docs/blog/General-Blogs/setup-puppeteer-and-use-in-Native-custom-APIs.html)
