# 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 inside a sandbox of its own with a Dockerfile which installs Chromium, or Playwright and Puppeteer on the native process.

Source: https://docs.apimaker.dev/v4/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.

| | Sandbox | Native process |
|---|---|---|
| Where the code runs | In sandboxes of a group of its own, in Docker | On the machine of the server (`runOnNativeProcess: true`) |
| Where the browser is installed | In the Dockerfile of the group : built once, on every server | On each server, next to API Maker |
| Isolation | Your code and the browser stay apart from API Maker | None : the browser shares the server with API Maker |
| Set up by | An admin, in the settings of the custom API | The root user of each server |

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 in the sandbox

The sandboxes of an account have no browser : nothing is installed that the account did not add. Give the APIs which drive one a [separate sandbox](https://docs.apimaker.dev/v4/docs/settings/customApiSettings.html#separate-sandbox) : a group with a Dockerfile which installs Chromium, and the package `playwright` in the same version. The other code of the account keeps its small image.

**Basic info : a group of sandboxes with Chromium**

```typescript
import * as T from 'types';

let customApi: T.ICustomApiSettingsTypes = {
    name: 'Invoice PDF',
    path: '/invoice-pdf',
    requestMethod: T.ERequestMethod.POST,
    apiAccessType: T.EAPIAccessType.TOKEN_ACCESS,
    customApiTimeoutInSeconds: 60,
    separateSandboxSettings: {
        enableSeparateSandboxForThis: 'browsers',   // every API which names this group shares its sandboxes
        sandboxCount: 1,
        packages: {
            allowAllPackagesOfAdmin: false,
            sandboxPackages: [{ name: 'playwright', version: '1.63.0' }],
        },
        dockerFileOfSandbox: `FROM {{AM_BASE_IMAGE}}

# Chromium and the system libraries it needs, in a folder every user of the image reads : built once, above the packages.
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
RUN npx -y playwright@1.63.0 install --with-deps chromium

{{AM_PACKAGES}}

{{AM_RUNNER}}`,
    },
    errorList: [],
};
module.exports = customApi;
```

**Code : HTML to PDF, sent as a download**

```typescript
import * as T from 'types';
const path = require('path');
const { chromium } = require('playwright');

async function main(g: T.IAMGlobal) {
    const file = `invoice-${g.req.body.number}.pdf`;
    const browser = await chromium.launch({ args: ['--no-sandbox'] });
    try {
        const page = await browser.newPage();
        await page.setContent(`<h1>Invoice ${g.req.body.number}</h1><p>Total : ${g.req.body.total} EUR</p>`);
        await page.pdf({ path: path.join(__dirname, 'uploads', file), format: 'A4', printBackground: true });
    } finally {
        await browser.close();
    }
    // A file of the uploads folder goes out as the answer, and is removed after.
    return { __am__downloadFilePath: file, __am__downloadFolderFileName: file };
}
module.exports = main;
```

- Saving the API builds the image of the group on every server, in the background : about a minute the first time, then never again while the Dockerfile and the packages stay the same. A call which comes before waits for it.
- Keep the version of the package and of `playwright install` the same : the package looks for the browsers of its own version.
- `--no-sandbox` : the container is the sandbox of Chromium.
- More APIs with a browser name the same group and share its image and sandboxes.

## 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 in the folder API Maker runs in (its `package.json` and `.env` are there) : the runners of the machine find their packages there.

    ```bash
    cd /path/of/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](https://docs.apimaker.dev/v4/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 :

**Custom API : HTML to PDF with Playwright**

```ts
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**
>
> What is installed by hand lives on one server : repeat both steps on every server of the cluster, and on each new one. The [sandbox](https://docs.apimaker.dev/v4/docs/guides/browser-automation.html#playwright-in-the-sandbox) has no such step : its image is built on every server from the settings.

## 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 in the folder API Maker runs in (its `package.json` and `.env` are there) : the runners of the machine find their packages there.

    ```bash
    cd /path/of/api-maker
    npm install puppeteer
    ```

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

**Custom API : HTML to PDF with Puppeteer**

```ts
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](https://docs.apimaker.dev/v4/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.
- The native process runs TypeScript with the `node` of the server : Node.js must be installed there, on every server.
- Code on the native process is not isolated from the server : prefer the sandbox when you can, and keep the native process for the APIs which need it.

## Related

- [Custom APIs](https://docs.apimaker.dev/v4/docs/apis-all/custom-apis/user-created-custom-api.html) · [Sandbox settings](https://docs.apimaker.dev/v4/docs/settings/sandboxSettings.html) · [How API Maker works](https://docs.apimaker.dev/v4/docs/getting-started/how-it-works.html#where-your-code-runs)
