npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ubhub/maps

v0.4.0

Published

Official UBHUB SDK for React and Next.js: vector maps with 3D buildings and trees, place search, typed geocoding, reverse geocoding and routing for Mongolia.

Readme

UBHUB Maps SDK for React and Next.js

@ubhub/maps is the official UBHUB SDK for the web. It has three parts:

  • @ubhub/maps: a typed client for geocoding, reverse geocoding and routing. It works in Node.js, edge runtimes and the browser.
  • @ubhub/maps/next: a route handler that keeps your API key on the server and proxies the endpoints your pages use.
  • @ubhub/maps/react: <UbHubMap>, the <UbHubPlaceSearch> combobox and data hooks.

It mirrors the Flutter SDK ubhub_maps: same endpoints, validation rules, error types and map defaults.

Full documentation: https://developers.ubhub.mn/developers/docs/web-sdk

Installation

npm install @ubhub/maps maplibre-gl

maplibre-gl (v4–v6) is only needed for <UbHubMap>. React 18 or 19 is needed for @ubhub/maps/react.

Why a server route?

UBHUB API keys are secret. Anyone who opens your page can read a key shipped to the browser. The API also refuses browser calls from other sites (CORS).

So the key lives in an environment variable on your server. Browser code calls a route in your own app, and that route adds the key.

Browser ──/api/ubhub/v1/...──▶ your Next.js route (adds key) ──▶ developers.ubhub.mn/v1/...

Quick start (Next.js App Router), step by step

Full walkthrough with every feature: https://developers.ubhub.mn/developers/docs/web-sdk

Step 1 — Install.

npm install @ubhub/maps maplibre-gl

Step 2 — Get an API key in your dashboard. Allow basemap (map), geocode (search) and route (routing).

Step 3 — Add the key to .env.local. Never use a NEXT_PUBLIC_ prefix (the key is a server secret).

UBHUB_API_KEY=geo_your_key_here

Step 4 — Add the proxy route in app/api/ubhub/[...path]/route.ts. It keeps the key on the server:

import { createUbHubHandler } from "@ubhub/maps/next";

export const { GET } = createUbHubHandler();

Step 5 — Import the styles once (for example in app/layout.tsx):

import "maplibre-gl/dist/maplibre-gl.css";
import "@ubhub/maps/styles.css";

Step 6 — Show a map with search. Give the map a height, or it collapses.

"use client";

import { useState } from "react";
import { UbHubMap, UbHubPlaceSearch, UbHubProvider, type UbHubMapController } from "@ubhub/maps/react";

export function Finder() {
  const [map, setMap] = useState<UbHubMapController>();

  return (
    <UbHubProvider proxyUrl="/api/ubhub">
      <UbHubPlaceSearch onSelect={(place) => map?.flyTo(place.location)} />
      <UbHubMap style={{ height: 480 }} onReady={setMap} />
    </UbHubProvider>
  );
}

Step 7 — Add markers. markers is a list of UbHubMarker; a bare { position } is the default dot.

<UbHubMap
  style={{ height: 480 }}
  markers={[
    { position: { latitude: 47.9184, longitude: 106.9177 } },
    { position: place.location, id: place.id, icon: { name: "pin", url: "/pin.png" }, label: place.name },
  ]}
/>

Next steps (see the guide for code): clustering (clusterMarkers), shapes (polylines, polygons, circles), taps (onMarkerTap, onClick, onLongPress), infoWindow, and camera (minZoom/maxZoom, cameraBounds, onCameraMove/onCameraIdle).

The proxy route

export const { GET } = createUbHubHandler({
  // Optional. Defaults shown.
  apiKey: process.env.UBHUB_API_KEY,
  baseUrl: "https://developers.ubhub.mn",
  endpoints: ["geocode", "route", "basemap"],
  timeoutMs: 10_000,
  // Recommended in production: only signed-in users may spend your quota.
  authorize: async (request) => Boolean(await getSession(request)),
});

The proxy forwards only these requests:

  • /v1/geocode with q, lat, lon and limit.
  • /v1/route with origin, destination and mode.
  • The Default and Nova basemap style.json (with its buildings and trees options), tiles, sprites and fonts.
  • The building and tree tiles, tree models, and the 3D buildings tileset.json and its tiles.

Anything else returns 404, and unknown query parameters return 400. It also:

  • does not forward cookies;
  • never follows redirects;
  • caps responses at 8 MiB.

It also serves MapLibre's worker script (/maplibre/maplibre-gl-worker.mjs) from your own maplibre-gl install, and the Draco decoder for the 3D buildings (/draco/...) from your @loaders.gl/draco install; this needs the Node.js runtime and uses no API quota.

It rewrites style.json and the 3D tileset.json so that every tile, sprite, font and model URL points back at the proxy. Any resource hosted elsewhere is rejected. The key never appears in a response.

It uses only the standard Request and Response objects, so it also works in Remix, Hono, Bun or any fetch-based server.

If your app runs behind a reverse proxy that changes the host, set publicUrl: "https://example.com/api/ubhub".

Each style.json and tileset.json request counts as one map load against your plan, the same as direct API calls. Tiles, sprites, fonts and models are not charged but are rate limited.

Server-side calls

Use the client directly in route handlers, server actions and server components:

import { UbHubGeo, NotFoundError } from "@ubhub/maps";

const ubhub = new UbHubGeo({ apiKey: process.env.UBHUB_API_KEY! });

const { results } = await ubhub.geocoding.search("Sukhbaatar Square", { limit: 5 });
const nearest = await ubhub.geocoding.reverse({ latitude: 47.9184, longitude: 106.9177 });

try {
  const { route } = await ubhub.routing.route({
    origin: { latitude: 47.9184, longitude: 106.9177 },
    destination: { latitude: 47.922, longitude: 106.9057 },
    mode: "driving", // or "walking"
  });
  console.log(route.distanceMeters, route.durationSeconds, route.geometry);
} catch (error) {
  if (error instanceof NotFoundError) console.log("No route between these points.");
  else throw error;
}

In the browser, create the client with new UbHubGeo({ proxyUrl: "/api/ubhub" }), or use useUbHub() inside a <UbHubProvider>. Passing apiKey in a browser throws.

Options

| Option | Default | Notes | | --- | --- | --- | | apiKey | | Server only. Exactly one of apiKey or proxyUrl. | | proxyUrl | | Path or URL of your createUbHubHandler route. | | baseUrl | https://developers.ubhub.mn | HTTPS origin; /v1 is appended. http://localhost is allowed for development. | | timeoutMs | 10000 | One deadline for sending the request and reading the whole body. | | coalesceRequests | true | Identical calls that are in flight at the same time share one HTTP request. | | fetch | global fetch | For tests or instrumentation. | | debug, logger | off | Logs GET /v1/<endpoint> status=<n> duration=<ms>ms only. |

Every method accepts { signal }. Aborting cancels only that caller; other callers sharing the request are not affected. close() aborts everything that is pending.

Validation

Invalid input throws TypeError or RangeError before any request is sent:

  • query must be 2–200 characters and not blank.
  • limit must be 1–20.
  • Latitude must be from −90 to 90 and longitude from −180 to 180, and both must be finite.
  • Coordinates may have at most 15 decimal places.
  • mode must be "driving" or "walking".

Hooks

const { results, isLoading, error, tooLong } = usePlaceSearch(text, { limit: 5, debounceMs: 350 });
const { data } = useReverseGeocode(clickedPoint); // null or undefined = idle
const { data: directions } = useRoute(from && to ? { origin: from, destination: to, mode: "walking" } : null);

Each hook returns { data, error, isLoading }. A newer input aborts the request for the older one, so a late answer never replaces a newer one.

<UbHubMap>

| Prop | Default | Notes | | --- | --- | --- | | basemap | "standard" | "nova" for the Nova style. Changing it swaps the style in place. | | buildings | "flat" | "none", "flat", "extruded", "detailed" (the detailed 3D buildings as extruded parts, MapLibre only) or "3d" (the meshes, needs scene3d). Changing it loads a new style (one map load). With "none", Nova keeps its own flat buildings. | | trees | "none" | "none", "points", "extruded" (trunk and crown shapes, MapLibre only) or "3d" (tree models, needs scene3d). | | buildingStyle | — | { color?, outlineColor?, opacity?, heightScale? }. Omitted values keep the basemap's building colors. Applies live. | | treeStyle | — | { color?, outlineColor?, opacity? } for tree dots. Applies live. | | showBuildings, showTrees | true | Hide or show the layers live, without a new map load. | | pitch | 0 | Camera tilt, 0–85. 0 keeps the map flat and locked; above 0 users can also tilt it. Applies live. | | scene3d | — | The detailed 3D renderer from @ubhub/maps/3d. | | initialCenter | Ulaanbaatar (47.9184, 106.9177) | Read once, when the map is created. | | initialZoom | 12 | Read once. Must be within minZoom–maxZoom. | | minZoom | 0 | How far users can zoom out (0–22). Changes apply live. | | maxZoom | 16 | How far users can zoom in (0–22). The basemap serves tiles up to zoom 16; above that MapLibre enlarges them. Changes apply live. | | markers | [] | UbHubMarker[]: { position, id?, icon?, label?, size?, data? }. A bare { position } is the default dot; icon: { name, url } draws a pin. | | polylines | [] | UbHubPolyline[]: { points, color?, width?, id?, data? }. | | polygons | [] | UbHubPolygon[]: { rings, fillColor?, fillOpacity?, strokeColor?, strokeWidth?, id?, data? }. | | circles | [] | UbHubCircle[]: { center, radiusMeters, … } — meter-accurate, scales with zoom. | | route | — | Convenience line: points (2 or more). | | clusterMarkers | false | Group markers into count bubbles; clusterRadius, clusterMaxZoom tune it. Tapping a cluster zooms to its expansion zoom (≤16). | | cameraBounds | — | UbHubBounds restricts panning. Applies live. | | dragPan, dragRotate, scrollZoom, doubleClickZoom, touchZoomRotate, keyboard | true | Gesture toggles. Apply live. | | onReady | — | Receives { map, flyTo(point, { zoom }), camera() }. map is the MapLibre instance. | | onMarkerTap, onPolylineTap, onPolygonTap, onCircleTap | — | Tap on an annotation (with its data). | | onClick, onLongPress | — | Basemap tap / long-press (right-click or touch). Not fired over a marker or shape. | | onCameraMove, onCameraIdle | — | Each receives a UbHubCameraPosition ({ center, zoom, bearing, pitch }). | | infoWindow | — | (marker) => ReactNode rendered in a popup above the tapped marker. Needs react-dom. | | onError | — | Fires only when the style cannot load. A missing tile just leaves a gap. | | navigationControl | true | Zoom and compass buttons. | | mapOptions | — | Extra MapLibre options. The map is 2D by default; set pitch to tilt it. | | workerUrl | proxy route | MapLibre v6 worker URL; false leaves MapLibre's setting alone. |

Migrating markers from GeoPoint[] (0.1.x → 0.2.0): markers.map((position) => ({ position })). A bare { position } renders the same dot.

Custom marker icons load from a URL: icon: { name: "pin", url: "/pin.png" }. Info windows need react-dom (already present in any React app).

Give the map a height, and import maplibre-gl/dist/maplibre-gl.css once in your app. The map uses MapLibre's attribution control; keep it visible.

MapLibre v6 decodes tiles in a web worker that bundlers do not copy into your build. The proxy route serves it from your installed maplibre-gl at /api/ubhub/maplibre/maplibre-gl-worker.mjs, and <UbHubMap> uses it automatically. With a Content Security Policy, allow worker-src 'self' blob:. To host the worker yourself, pass workerUrl. With output: "standalone", add node_modules/maplibre-gl/dist/maplibre-gl-worker.mjs to outputFileTracingIncludes for the route.

3D buildings and trees

Buildings and trees are separate layers you can style, hide or leave out:

<UbHubMap
  buildings="detailed"          // none | flat (default) | extruded | detailed | 3d
  trees="extruded"              // none (default) | points | extruded | 3d
  pitch={55}
  buildingStyle={{ color: "#1d4ed8", heightScale: 1.5 }}
  treeStyle={{ color: "#22c55e" }}
  showBuildings={showBuildings} // toggles live
  showTrees={showTrees}
  style={{ height: 600 }}
/>

For the detailed building meshes and tree models (the same 3D view as developers.ubhub.mn), install the 3D renderer's peers and pass it as scene3d:

npm install @deck.gl/core @deck.gl/layers @deck.gl/geo-layers @deck.gl/mesh-layers @deck.gl/mapbox \
  @loaders.gl/core @loaders.gl/3d-tiles @loaders.gl/gltf @loaders.gl/draco @loaders.gl/mvt
import { ubhub3d } from "@ubhub/maps/3d";

<UbHubMap buildings="3d" trees="3d" scene3d={ubhub3d} pitch={55} style={{ height: 600 }} />
  • The 3D renderer lives in its own entry point. Apps that do not import @ubhub/maps/3d need none of these packages, and their bundle does not grow.
  • "detailed" and "extruded" give a close 3D look with MapLibre alone: the detailed buildings as parts (wings, towers, roof steps) each raised to its own height, and trees as a trunk and a crown. Sloped and curved roofs become steps. Where the API has none yet, they fall back to "extruded" buildings and tree "points".
  • Without scene3d, "3d" falls back to "detailed" buildings and "extruded" trees, with a console warning.
  • buildingStyle.color also colors the 3D meshes (#rgb or #rrggbb). The tree models keep their own colors.
  • Performance. "none" leaves a layer and its tiles out entirely. A hidden layer (showBuildings={false}) loads no new tiles. Keep extruded buildings opaque (opacity 1). The detailed meshes keep up to 256 MB of GPU memory for the city center.
  • The Draco decoder for the meshes is served by your proxy route at /api/ubhub/draco/. With a Content Security Policy, allow worker-src 'self' blob: and script-src 'wasm-unsafe-eval'. With output: "standalone", add node_modules/@loaders.gl/draco/dist/** to outputFileTracingIncludes for the route.

<UbHubPlaceSearch>

An accessible combobox following the WAI-ARIA 1.2 pattern:

  • Keyboard: Arrow keys move through results, Enter picks the highlighted result (or the first one), and Escape closes the list, then clears the text.
  • Searching: it searches from 2 characters with a 350 ms debounce and ignores stale responses.
  • Status messages: it shows a short status line, such as "No places found." or "Search limit reached. Please try again later.".

Props: onSelect, onError, limit, debounceMs, placeholder, label, disabled, defaultValue and className.

Import @ubhub/maps/styles.css for the default look, or style the ubhub-search* classes yourself. The CSS variables --ubhub-accent, --ubhub-bg, --ubhub-fg, --ubhub-muted, --ubhub-border and --ubhub-hover cover colors.

Errors

Every failure after a request starts is a UbHubError with status, code and requestId. Messages are fixed SDK text; server error text is never copied.

| Error | When | | --- | --- | | ValidationError | 400 or 422 | | AuthenticationError | 401: missing, invalid or expired key | | PaymentRequiredError | 402: no active plan | | AuthorizationError | 403: revoked key, or the key lacks this endpoint | | NotFoundError | 404: for example, no route | | TimeoutError | 408, or the SDK deadline expired | | RateLimitError | 429. Has retryAfterMs, limit, remaining and reset. | | ServerError | 5xx | | HttpError | Any other status, including redirects | | NetworkError | The connection failed | | ResponseError | Invalid JSON, unexpected shape, or a body over 8 MiB |

The SDK never retries; a RateLimitError tells you how long to wait. Quote requestId when you contact support.

Development

npm install
npm test          # unit tests (vitest)
npm run typecheck
npm run lint
npm run build     # dist/ (ESM, CJS, .d.ts)
npm run check:contract -- ../api_provider/packages/types/openapi.json

examples/nextjs is a small Next.js app that uses the local build.

License

MIT