Creating a static map renderer using the Mapbox GL Native NodeJS API

South Atlantic Conservation Blueprint v 2.2; Basemap: © Mapbox, © OpenStreetMap

We recently started using Mapbox GL JS in our frontend mapping and data visualization projects. It has proven to be a very powerful map engine, and we have appreciated how much it has allowed us to introduce some of the newer mapping technologies such as vector tiles to our stack.

For two of our recent projects, we needed to create downloadable reports in MS Word format, with embedded map images of the user’s area of interest. In one of our previous Leaflet applications, we used leaflet-image and a stack of other components to render the maps to images in PDF files generated in the frontend. However, we found this approach to be quite brittle, and limited to a subset of modern browsers.

For these new projects, we needed a backend service to generate map images using vector tiles and the same styles we use in our frontend. Unfortunately, the Mapbox static API didn’t meet our needs because our tiles are not hosted on Mapbox due to privacy and cost. We use a lightweight tile map server called mbtileserver for hosting our image and vector tiles on either tiny AWS EC2 instances or internal servers depending on the project. All we needed was an easy way to consume and render those tiles on the backend, and output map images.

When we looked around, we found that there wasn’t an existing solution that met our needs particularly well. We needed to:

  • provide a dynamic style to highlight specific features selected by the user from a vector tileset
  • provide a bounding box rather than the more standard center and zoom, so that we could fit the map to the user’s area of interest
  • include GeoJSON data sources in the style to highlight specific areas
  • deploy this to AWS EC2 instances or using Docker containers to servers that might be outside our direct control

Once we started investigating the Mapbox GL Native NodeJS API, we realized that we could render local mbtiles tilesets by fetching the tiles directly from the mbtiles file instead of via a tile service, which ends up being much faster during rendering. Mbtiles is a great format for storing map tiles. It is a self-contained sqlite database that contains map tile data (image or vector) and associated metadata, which makes it very easy to move tiles around between different machines.

Prior art:

One of the existing solutions include TileServer GL. This looks to be a powerful application based on Mapbox GL for hosting tile and static map services, and is quite full featured, but didn’t have the specific features we were looking for. In particular, we needed to be able to pass in styles dynamically on request, rather than providing them at server startup.

Another application we found is Atto, which uses Mapbox GL to render maps into PDF files. I’m particularly impressed that this was written in Go (my preferred implementation for an application like this) and the amount of glue code to allow using Mapbox GL native from Go. Unfortunately we needed maps we could render into MS Word documents.

Core architecture:

The initial core of the application was roughed out by one of our lead engineers, Nik Stevenson-Molnar. It leveraged Mapbox GL Native NodeJS API as the rendering engine, provided a lightweight REST API for generating map images, and used local mbtiles tilesets where possible. Initially, this was developed as a component within a larger private application, but we found we needed to generalize this component so that we could use it in other projects.

This led me to adapt it to create mbgl-renderer as an open source project, and significantly expand on the core functionality.

The application now consists of:

It allows you to pass in a style as part of the rendering operation, which can include GeoJSON sources. You can provide map bounds or center and zoom depending on your particular application. And you can use Mapbox hosted styles if you want to.

Our primary use of this so far has been the REST API. However, one of my favorite parts of the system to build was the command line interface. I find that having a command line interface makes it easier to define the core API of the application and makes it easier to test it while you are building it. For similar applications in other languages, I often build the command line interface before the web interface, even if the application doesn’t necessarily need a command line interface long term. I really like being able to fire up a command and get back a map:

mbgl-render tests/fixtures/example-style.json test.png 512 256 -c -79.95,32.79 -z 9

Local mbtiles sources and tiles:

As we were developing this library, we determined that it would be great if we could easily render locally-hosted mbtiles files. To do this, we had to figure out how to use the options parameter of the Mapbox GL map instance in to provide a custom request handler that would use our local mbtiles files instead of fetching tiles from a remote tile service. The existing documentation of the options parameter is a bit lean on this front, so at this point I would refer folks to look over our more complete example to get a better understanding about how requests for different type of assets in a style work.

Basically, the heart of the options parameter is a request handler: it allows you to make requests differently based on url and kind where the latter tells you if the resource is a source, tile, sprite, glyph, etc.

We were able to leverage this to render from our local mbtiles files:

  • if kind is a source, we connect to our mbtiles file and generate a TileJSON object from the metadata in the file. This TileJSON is automatically processed by the Mapbox GL map to determine the tiles to request, and it generates new tile requests that we catch in the next bullet.
  • if kind is a tile, we connect to our mbtiles file and read the data for that tile if it exists. If the tile is a vector tile, we have to unzip it before returning it for rendering.

To distinguish our local mbtiles files, we used the url scheme mbtiles:// and parsed that in our request handlers to determine the name of our mbtiles file. Note: the path to these mbtiles needs to be registered with mbgl-server when it is started up. See the following example style files to see local mbtiles files in action:

Using Mapbox hosted sources and styles:

One of the more interesting challenge was resolving mapbox:// URLs to full URLs that we could request over the web. Luckily, I was able to use the Mapbox API documentation and draw inspiration from a utility script in the Mapbox GL JS repository. Resolving these URLs allows us to use Mapbox hosted styles, sources, tiles, sprites, and glyphs, which is a nice win for this project. For example:

Sources: resolve mapbox://mapbox.mapbox-streets-v7 to

https://api.mapbox.com/v4/mapbox.mapbox-streets-v7.json?secure=true&access_token=<your token>

Styles: resolve mapbox://styles/mapbox/outdoors-v10 to

https://api.mapbox.com/styles/v1/mapbox/outdoors-v10?secure=true&access_token=<your_token>

To use a couple layers from the Mapbox streets style, include that source and layers in your style file (example):

mbgl-render tests/fixtures/example-style-mapbox-source.json test.png 512 512 -c 0,0 -z 0 --token <your token here>
© Mapbox, © OpenStreetMap

This allows you to mix and match Mapbox hosted sources with your own, without having to host everything on Mapbox. Not that you shouldn’t do that, they offer a pretty awesome service if it fits your needs.


The biggest challenge I encountered while working on this project was that the Mapbox GL Native Node library needed to be rebuilt from source every time. This makes what is normally a fast and simple yarn install slow and full of peril if your environment is not properly setup to build it from source. However, once you have everything setup properly, being able to use the Mapbox GL Native NodeJS API allows you to create beautiful static maps with the latest map technologies. Enjoy!