UzMap docs

Browser SDK

https://uzmaps.ndc.uz/v1/uzmaps.js is the map platform as one classic <script> tag. The file carries @uzmaps/engine, @uzmaps/api, @uzmaps/cartography and MapLibre GL JS, injects its own stylesheet, and reads the API key and the server address off the tag that loaded it. It exists for pages that have no bundler: a CMS template, a landing page, a dashboard maintained as HTML.

html
<div id="map" style="width:100%;height:400px"></div>

<script src="https://uzmaps.ndc.uz/v1/uzmaps.js?apikey=YOUR_KEY"></script>
<script>
  uzmaps.ready(function () {
    var map = new uzmaps.Map({
      container: 'map',
      center: [69.2401, 41.2995], // [lon, lat]
      zoom: 12
    });
  });
</script>

The script tag#

Three things are read from the tag at the moment the script executes, via document.currentScript. They are read then, and not later, because document.currentScript is only non-null while the script is running.

Taken from the tagBecomesNotes
?apikey= query parameterthe default API key?key= is accepted as an alias, because it is the name the REST API uses. When both are present apikey wins.
the script URL's origin (scheme, host, port)the default server, e.g. https://uzmaps.ndc.uzOnly the origin: the script lives under /v1/, the API does not.
the script URL's directory, e.g. https://uzmaps.ndc.uz/v1/where MapLibre's worker is fetched frommaplibre-gl-worker.mjs and maplibre-gl-shared.mjs must sit beside uzmaps.js.

Two rules follow from how this is read:

  • Load it as a classic script with a src attribute. Inside a module script document.currentScript is null, and the bundle is an IIFE that assigns to this.uzmaps, so type="module" breaks it. Pasting the file's contents into an inline <script> also loses the key, the server and the worker location: with no src there is nothing to read, and the SDK falls back to an empty server URL and no key.
  • Without a key the script still loads and uzmaps.defaults.apiKey is undefined. On a server running with --require-key, tiles and API calls are then refused. See API keys.

How the key travels matters for origin allowlists and access logs:

  • API calls under /api/ send it as an X-API-Key header.
  • Tiles, glyphs and sprites are fetched by MapLibre itself, where the SDK cannot set headers, so the map appends ?key= to those URLs. It does this only for URLs that start with the configured server, so a source you point at a third party never receives your key.
  • /v1/ itself is never key-gated. The script has to load before it can present a key, and a JSON 401 where the browser expects JavaScript is a syntax error, not a diagnosis.

What is on the global#

The bundle assigns its named exports onto window.uzmaps. There is no uzmaps.default: a default export in an IIFE build would land one level deeper than everything else, and that mismatch is invisible to the type checker.

MemberTypeWhat it is
uzmaps.Mapclass, new Map(options: UzMapOptions)UzMap from @uzmaps/engine with apiKey and serverUrl filled in from the tag when you do not pass them. It is a real subclass, so map instanceof uzmaps.UzMap holds and every engine method and event works unchanged.
uzmaps.Clientclass, new Client(options?: ClientOptions)UzMapClient from @uzmaps/api with apiKey filled in. Read the note on baseUrl below before using it.
uzmaps.ready(fn?)(fn?: () => void) => Promise<void>Resolves once the DOM is ready.
uzmaps.versionstringThe bundle's version, taken from @uzmaps/engine's package.json at build time. 0.1.1 in the current build.
uzmaps.defaults{ serverUrl: string; apiKey: string | undefined; dir: string }What was read from the script tag.
uzmaps.buildStyle(opts?)(opts?: StyleOptions) => StyleSpecificationThe cartography's style generator, for driving MapLibre directly. Takes serverUrl, theme, language and the other StyleOptions fields; nothing is filled in from the tag.
uzmaps.themesRecord<'light' | 'dark', ThemeTokens>The two colour token sets the styles are built from.
uzmaps.UzMap, uzmaps.UzMapClientclassesThe underlying engine and API classes, without the script-tag defaults.

Overriding the defaults per map#

Pass apiKey or serverUrl and the tag's value is ignored for that instance, so one page can talk to two deployments or use two keys:

js
var other = new uzmaps.Map({
  container: 'map2',
  apiKey: 'OTHER_KEY',
  serverUrl: 'https://maps.example.uz'
});

The defaults are applied only when the option is undefined; an empty string or any other value you pass is kept.

uzmaps.Client and the server address#

The key is filled in for a Client exactly as for a Map. The server is not. ClientOptions calls it baseUrl, and the SDK fills baseUrl in only when the options object already has a baseUrl property and its value is empty or undefined. new uzmaps.Client() therefore receives the key from the tag but keeps baseUrl as '', which UzMapClient treats as the page's own origin. For a page that embeds the script from uzmaps.ndc.uz, that is the wrong host: every call goes to your own site instead of the map server.

This was confirmed by loading the built bundle from a page on a different origin: new uzmaps.Client().baseUrl is '', while new uzmaps.Client({ baseUrl: undefined }).baseUrl is the script's origin.

Two ways that work:

js
// 1. A map already carries a client configured with its own server, key and language.
var api = map.api;

// 2. A standalone client: name the server explicitly.
var api = new uzmaps.Client({ baseUrl: uzmaps.defaults.serverUrl });

map.api is the readonly api: UzMapClient the engine constructs from the map's serverUrl, apiKey and language.

uzmaps.defaults#

FieldValue
serverUrlThe script's origin, e.g. https://uzmaps.ndc.uz. '' when the script was not loaded through src.
apiKeyThe apikey (or key) parameter; undefined when absent.
dirThe script's directory with its trailing slash, e.g. https://uzmaps.ndc.uz/v1/.

When a key appears not to work, console.log(uzmaps.defaults) is the first check: it shows what the tag actually supplied.

ready()#

ts
uzmaps.ready(fn?: () => void): Promise<void>

If document.readyState is 'loading', it waits for DOMContentLoaded; otherwise it resolves on the next microtask. With fn it returns promise.then(fn); without, the bare promise, so await uzmaps.ready() works as well.

It exists because the tag is often placed in <head>, where the script runs before <div id="map"> exists, and new uzmaps.Map({ container: 'map' }) would then have no container to find. It is also the one stable place to put your code should the SDK ever need asynchronous initialisation.

It is DOM readiness, not map readiness. For the map itself use its events, for example map.on('load', fn).

html
<head>
  <script src="https://uzmaps.ndc.uz/v1/uzmaps.js?apikey=YOUR_KEY"></script>
  <script>
    uzmaps.ready(function () {
      // Without center and zoom the map opens on Tashkent ([69.2797, 41.3111]) at zoom 12.
      var map = new uzmaps.Map({ container: 'map' });
      map.on('load', function () { console.log('map loaded'); });
    });
  </script>
</head>

Searching and routing#

js
uzmaps.ready(async function () {
  var map = new uzmaps.Map({ container: 'map', center: [69.24, 41.29], zoom: 12 });
  var api = map.api;

  var found = await api.search('Chilonzor');          // SearchResponse
  if (found.results.length) {
    map.selectPlace(found.results[0], { fly: true });
  }

  var trip = await api.route({                        // RouteResponse
    points: [[69.24, 41.29], [69.28, 41.31]],         // [lon, lat]
    mode: 'car'
  });
  map.showRoutes(trip.routes);
});

search(query, options?) returns { query, results: SearchResult[], took_ms, intent? }. route(request) returns { routes: Route[], language, engine, warnings? }; mode is one of 'car' | 'foot' | 'bike' | 'taxi' | 'truck' | 'motorcycle'. selectPlace(place, { fly?, geometry? }) highlights a place and flies to it unless fly: false. showRoutes(routes, waypoints?, { selectedId?, fit?, fitPadding? }) draws the routes and frames them. Both the map and the client default to language: 'uz'; the other values are 'uz-Cyrl', 'ru' and 'en'.

Versioning under /v1/#

The server mounts the built SDK directory at /v1/ with http.StripPrefix("/v1/", sdkHandler(dir)) and serves four files:

FilePurpose
/v1/uzmaps.jsThe bundle.
/v1/uzmaps.js.mapSource map; fetched by devtools only.
/v1/maplibre-gl-worker.mjsMapLibre's worker, fetched when the first map is created.
/v1/maplibre-gl-shared.mjsThe worker's shared chunk, imported by the worker.

The prefix is the compatibility promise. The URL in a page is not something its owner will revisit, so the stated policy is that a breaking change is published under a new prefix while /v1/ keeps serving what it serves today. Within /v1/, uzmaps.version says which build a page has; it tracks the @uzmaps/engine release the bundle was built from.

The worker files are there because MapLibre normally finds its worker through import.meta.url, which an IIFE does not have. The SDK therefore calls setWorkerUrl(defaults.dir + 'maplibre-gl-worker.mjs'), and the build copies the worker and everything it imports into dist beside the bundle. If they are missing, the map hangs on a blank canvas with nothing but a MIME-type complaint in the console.

Caching#

Headers are set by the Go handler (sdk.go, asserted in sdk_test.go). nginx passes them through untouched: its /v1/ block deliberately adds no Cache-Control of its own, so the no-store rule for the app shell does not end up on the same response (deploy/web/server/nginx.conf).

PathCache-ControlWhy
/v1/uzmaps.jspublic, max-age=3600, stale-while-revalidate=86400It cannot be immutable: pages that will never be edited depend on this URL, so a fix has to reach them. It should not be uncacheable either, or every visit downloads it again. An hour bounds how long a fix takes to arrive.
/v1/*.mappublic, max-age=3600Devtools only.
everything else (the .mjs chunks)public, max-age=31536000, immutableThese change only when a new bundle ships.

Every /v1/ response also carries Access-Control-Allow-Origin: *: a CDN script is loaded cross-origin by definition, and the worker started from it inherits that.

In practice a returning visitor may run the previous uzmaps.js for up to an hour after a release, and can be served it stale for a day while the browser revalidates. No cache-busting parameter is needed on the tag.

The injected stylesheet#

The bundle inlines maplibre-gl/dist/maplibre-gl.css (MapLibre GL JS 6.7.0) and, when it runs, prepends <style id="uzmaps-styles"> to <head>. There is no <link> to add.

  • Once per page. The element is looked up by id before it is created, so a page that includes the script twice (a CMS and a widget, say) gets one copy rather than stacked duplicate rules.
  • Prepended, not appended, so your own stylesheets come later in the cascade and win at equal specificity. Override MapLibre's control styling with ordinary CSS.
  • The id is fixed. If you place your own element with id="uzmaps-styles" in the page, the SDK will assume its stylesheet is present and skip injection.

Size#

Measured from npm run build:sdk on the current source (@uzmaps/engine 0.1.1, MapLibre GL JS 6.7.0). The build configuration itself states no size: chunkSizeWarningLimit: 3000 in apps/sdk/vite.config.ts is only the threshold for a build warning, raised because the bundle legitimately contains MapLibre.

FileBytesgzip
uzmaps.js1,153,801292,869
maplibre-gl-worker.mjs19,1816,167
maplibre-gl-shared.mjs492,183137,710
uzmaps.js.map2,609,590524,682

So about 1.15 MB (293 kB gzipped) to load the script, and the first map then fetches the two worker files, about 0.5 MB more (144 kB gzipped). Nothing is left external: MapLibre is inside uzmaps.js and the page needs no other library. The gzip column is what a proxy that compresses JavaScript would send; the raw column is what crosses the wire otherwise.

The build targets ES2020, wider than the demo app's ES2022, because the file lands on pages nobody on this project controls.

Hosting the SDK yourself#

The bundle ships inside the server image: services/server/Dockerfile runs npm run build:sdk, copies apps/sdk/dist to /app/sdk, and starts uzmap serve with --sdk /app/sdk. --sdk names the directory. If it is empty, or does not contain uzmaps.js, the server starts anyway and /v1/ is not served, because the API and the app do not depend on it.

If you put uzmaps.js somewhere other than the API server, a CDN or your own static host, keep the four files together, and set serverUrl on every map and baseUrl on every client: the inferred server would otherwise be the static host.

When to use npm instead#

/v1/uzmaps.jsnpm install @uzmaps/engine
Needs a bundlerNoYes
Key and serverInferred from the tagPassed as apiKey and serverUrl
MapLibreInlined, pinned to the bundle's versionInstalled as a dependency, one copy shared with the rest of your app
CSSInjectedimport 'maplibre-gl/dist/maplibre-gl.css' yourself
TypesNoneTypeScript declarations in the package
PublishedServed by the map server; the @uzmaps/sdk-bundle workspace is private@uzmaps/engine 0.1.1 on npm

Both are the same code. uzmaps.Map is UzMap, so an integration can move from the tag to npm without changing a call. Use npm when you already have a build; use the tag when you do not.

Where each statement was checked#

Section
Script tag, key and server inference, defaults, ready(), stylesheet injection, global members
UzMapOptions, map.api, map.on, default centre and zoom, ?key= on tile URLs
ClientOptions, X-API-Key, search, route, response types, language values
buildStyle, themes
Bundle format, version define, worker copy, ES target
Mount at /v1/, not gated, --sdk flag
Cache headers, CORS header, missing-directory behaviour
nginx pass-through
Image build and --sdk /app/sdk
Sizes
npm publication and version

Map data © OpenStreetMap contributors, licensed under the ODbL. © 2026 National Development Community.