Quickstart
Zero to a rendered map in under a minute. The clock starts at npm i and stops when the map paints.
After that, this page is one small app that grows: a map, then a place, then a pin, then a route, then a look of its own. Each step is a complete file, each one adds a line or two to the one before it, and each one is shown running on this page: the code beside every example is the code that produced it.
New to maps APIs? Nothing below assumes you have used one, but if a word like tile, style, or isochrone goes past you, the one-minute glossary on the Overview defines the seven that matter.
You do not need an account to follow this page. Every example below runs on a public demo key,
and the last section gives you that key to paste into your own file. The
snippets show key: "um_live_..." as a placeholder: swap in the demo key to run them today, or
your own when you have one. Keys, allowed origins and what a rejected one looks like are
on Authentication.
1. Install
maplibre-gl comes along as a dependency of @unmap/sdk, so there is nothing else to install for a basic map.
If your page has no build step, load the same SDK from a script tag instead:
That is the whole file. The bundle carries its own MapLibre, so there is nothing else to add and nothing to configure.
The packages are optional. Every endpoint is plain HTTPS and JSON, so if you are not in JavaScript, or would rather not add a dependency, Calling the API directly covers curl, fetch, Python, and a stock MapLibre with no @unmap code at all.
2. Add a container
Anywhere in your page, give the map a box with a height:
<div id="map" style="height: 400px"></div>The height is not decoration. A container with no height renders a map of zero pixels, MapLibre reports no error, and the page simply looks empty. It is the first thing to check when nothing appears.
In your JavaScript or CSS entry, import MapLibre's stylesheet from the installed package:
import "maplibre-gl/dist/maplibre-gl.css";Without a bundler, link the same file from your HTML instead. Either way the stylesheet is served from your own site, not a third-party CDN. One caveat: under pnpm's strict layout a transitive dependency is not importable from your app, so add maplibre-gl as a direct dependency there.
3. Render a map
Two code statements after install. This is the whole Hello World, and it is running below:
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});That paints a Calgary basemap, the background map your own markers and data will sit on. center
and zoom say where it starts; style, mode and lang say how it is drawn. None of them is
required. Leave them out and you get base in light mode, with labels in the reader's own
language.
center is [longitude, latitude], in that order. Every coordinate in unmap is, and it is the
mistake that costs newcomers the most time: longitude is the east and west number, negative in
Canada. Swap them and the map lands in the Indian Ocean.
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
style: "base",
mode: "light",
lang: "fr",
center: [-73.5673, 45.5017],
zoom: 12,
});Press a control above and watch the snippet change with it: every option you choose appears in the code, and nothing you did not choose is hiding in it. The twelve styles and both modes are covered on Maps API.
That is the 60-second mark. Everything below is optional.
If the map never finishes loading
One bundler problem is worth knowing about before it costs you an afternoon, because it fails quietly. If your map renders a grey box and never paints (no error thrown, no failed request in the network panel), open the console and look for this:
Failed to load module script: The server responded with a non-JavaScript MIME type of "text/html".
MapLibre GL JS 6 is ESM-only and does its tile parsing in a web worker, which it loads as a module.
It finds that worker through import.meta.url, and MapLibre's own
migration guide says
plainly that the value does not reliably survive a bundler's module graph. When it does not
resolve, the worker never starts, MapLibre never parses a tile, so it never requests one, and the
map waits forever for a load event that cannot arrive. Nothing in your code is wrong, and nothing
in your code can catch it.
The fix is one call, and MapLibre asks every bundler user to make it. Copy the worker and the chunk it imports out of the package and serve them yourself:
// scripts/copy-maplibre-worker.mjs
import { copyFileSync, mkdirSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
const dist = path.join(path.dirname(createRequire(import.meta.url).resolve("maplibre-gl/package.json")), "dist");
const dest = path.join(process.cwd(), "public", "maplibre");
mkdirSync(dest, { recursive: true });
for (const file of ["maplibre-gl-worker.mjs", "maplibre-gl-shared.mjs"]) {
copyFileSync(path.join(dist, file), path.join(dest, file));
}Run it before both dev and build, then point MapLibre at the copies once, above the first map
you create:
import { maplibregl } from "@unmap/sdk";
maplibregl.setWorkerUrl("/maplibre/maplibre-gl-worker.mjs");Copy both files. The worker imports maplibre-gl-shared.mjs by a plain relative path, so it
has to sit beside the worker under that exact name. Copying only the worker reproduces the same
silent failure one step later.
Next.js with Turbopack is the case most people meet this in (maplibre-gl-js#8126), and it is what unmap.dev itself runs. Vite, webpack, esbuild, rspack and Rollup want the same call with their own path. Loading MapLibre straight from a CDN as a module needs none of this: it resolves the worker on its own. Neither does the script tag build above: it ships its own worker and points MapLibre at it before your first map is created.
4. Find a place
.geocoder is on the same instance, and it works with or without a map:
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({ key: "um_live_..." });
const results = await unmap.geocoder.search("Calgary Tower", { limit: 3 });Every call resolves to an array of results. Each result carries id, name, layer, lng, lat, a names map when the place has names in more than one language, and a structured address when one exists. Search and autocomplete results add a score; category results add a distance in metres. The full shape is on the Geocoding API page.
The other three lookups take the same shape:
// Type-ahead (fires on each keystroke)
const hints = await unmap.geocoder.autocomplete("rue sainte-cath", {
lang: "fr",
limit: 5,
});
// Category search: the nearest pharmacies to a point, sorted by distance
const nearby = await unmap.geocoder.nearby("pharmacy", {
near: [-114.07, 51.05],
limit: 5,
});
// Reverse geocode a coordinate
const here = await unmap.geocoder.reverse(-114.07, 51.05);Geocoding on its own
No map, no full SDK: @unmap/geocoding is a standalone client for the same four endpoints, with no dependency on MapLibre.
import { Geocoder } from "@unmap/geocoding";
const geocoder = new Geocoder({ key: "um_live_..." });
const results = await geocoder.search("Iqaluit");
const hints = await geocoder.autocomplete("yellowkni", { limit: 5 });
const cafes = await geocoder.nearby("café", {
near: [-73.5673, 45.5017],
radius: 1000,
});
const here = await geocoder.reverse(-68.517, 63.749);5. Put the place on the map
Steps 3 and 4 together: geocode a query, then drop a MapLibre marker on the answer. unmap.map is
the MapLibre GL map, so anything the MapLibre API can do to a map, you can do to this one.
import { Unmap, maplibregl } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.0719, 51.0447],
zoom: 11,
});
const [place] = await unmap.geocoder.search("Calgary Tower");
new maplibregl.Marker({ color: "#FF3E9A" })
.setLngLat([place.lng, place.lat])
.setPopup(new maplibregl.Popup().setText(place.name))
.addTo(unmap.map!);6. Draw a route
.router is the third thing on the same instance. route() returns the distance, the duration, and
a GeoJSON LineString you can add to the map as a source:
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
center: [-114.03, 51.07],
zoom: 11,
});
const route = await unmap.router.route([-114.0719, 51.0447], [-113.9871, 51.0899], { mode: "auto" });
unmap.map!.on("load", () => {
unmap.map!.addSource("route", {
type: "geojson",
data: { type: "Feature", properties: {}, geometry: route.geometry },
});
unmap.map!.addLayer({
id: "route-line",
type: "line",
source: "route",
layout: { "line-cap": "round", "line-join": "round" },
paint: { "line-color": "#FF3E9A", "line-width": 3 },
});
});Coordinates are [lng, lat] here too. distanceMeters and durationSeconds are exactly what the
names say: metres and seconds, not kilometres or minutes. isochrone() answers the other routing
question, how far can I get in a given time, and returns that area as a GeoJSON FeatureCollection:
const bands = await unmap.router.isochrone([-114.07, 51.05], {
minutes: [10, 20],
});minutes is required, takes up to four values, and each must be 120 or less.
Routing on its own
No map, no full SDK: @unmap/routing is a standalone client for the same two endpoints.
import { Router } from "@unmap/routing";
const router = new Router({ key: "um_live_..." });
const route = await router.route([-114.07, 51.05], [-113.99, 51.05], {
mode: "auto",
});
console.log(route.distanceMeters, route.durationSeconds);
const bands = await router.isochrone([-114.07, 51.05], {
minutes: [10, 20],
mode: "pedestrian",
});mode is 'auto' | 'car' | 'bicycle' | 'pedestrian' | 'truck' | 'transit', in the published @unmap/routing types and on the gateway alike (car is the same driving profile as auto; see the Routing API for the truck vehicle profile). The default is 'auto'.
7. Give it a look of its own
The last option is theme: a code from the theme builder that redraws the map in
colours you chose. It is data, not a stylesheet: it rides in the same request as everything
else, and it wins over style.
import { Unmap } from "@unmap/sdk";
const unmap = new Unmap({
key: "um_live_...",
container: "map",
theme: "u4e",
center: [-114.0719, 51.0447],
zoom: 12,
});u4e is a short code for a shipped theme preset; the builder also exports long ut1. codes for a
theme you composed yourself. Both go in the same option. Maps API
covers what a theme may carry.
Errors
The clients throw on any non-2xx response. GeocoderError and RouterError both carry the HTTP status, so you can branch on it:
import { GeocoderError } from "@unmap/sdk";
try {
await unmap.geocoder.search("");
} catch (e) {
if (e instanceof GeocoderError && e.status === 400) {
// the request was malformed; see /docs/reference/errors for every status
}
}Using a demo key
Want to try before you get a key? The public demo key is rate-limited and safe to embed. It is the one every example on this page is running on:
new Unmap({
key: "um_test_793cf0c9cb8f4f5cf12bbdebdd96b59532b38876fbe5d14d",
container: "map",
center: [-68.517, 63.7467],
zoom: 11,
});It talks to the same gateway your own key will, https://api.unmap.dev; that is the default, so there is no gateway option to set. Because the key is shared, it can hit the rate limit at busy moments. Get your own from the dashboard when you are ready.
Next steps
- Browse the Examples for a live map, a lookup, or a route you can copy.
- Switching from Google Maps, Mapbox, HERE, or Esri? Start at Migrate.
- Read the Overview for the full option and method reference, and the list of packages.
- Authentication for key forms, allowed origins, the browser-versus-server split, and what a rejected key returns.
- Calling the API directly if you would rather skip the packages; the Maps API, the Geocoding API, and the Routing API document every endpoint, parameter, and response.
- On React or Vue with shadcn?
npx @unmap/cli create my-appscaffolds a project with a map, search box and routing panel already in it, or copy them into an existing project from the component registry. - Design your own cartography at /create and pass the code as
theme.