@maimaps/js · v0.1.1 · Beta

Maimaps JS SDK

Core JavaScript/TypeScript client for search, routing, reverse geocoding, point details, loccode, and keyed map styles.

Package

@maimaps/js

Version

v0.1.1

Registry

npm

Platforms

JS / TypeScript • Browser, Node 18+

Requires

No runtime dependencies

Status

Beta — 0.x, minors may break

Overview

Pure TypeScript with zero runtime dependencies, distributed as dual ESM/CJS with full type declarations. It talks to the keyed Maimaps SDK API (/sdk/v1 REST plus the /sdk map-display routes) and is the foundation the React and React Native SDKs are built on. It normalizes the API's mixed snake_case and camelCase responses into consistent camelCase, and turns HTTP failures into typed errors.

Every endpoint this SDK wraps is documented, with request parameters and response bodies, in the Maimaps API reference. This page documents the SDK surface itself.

Install

Terminal
npm install @maimaps/js

Requires nothing else. The core client has no runtime dependencies.

Quickstart

Replace mk_test_your_key with a MAIMAPS key from the console. Keys are mk_live_… for LIVE and mk_test_… for TEST; nothing else in your code changes between the two.

quickstart.ts
import { MaimapsClient } from "@maimaps/js";
import maplibregl from "maplibre-gl";

const client = new MaimapsClient({ apiKey: "mk_test_your_key" });

// Render a map — each keyed style.json fetch counts as one map load.
const map = new maplibregl.Map({
  container: "map",
  style: client.styleUrl({ mode: "dark" }),
  transformRequest: (url) => ({ url: client.transformMapResource(url) }),
});

// First search
const results = await client.search({
  q: "garki market",
  latitude: 9.0579,
  longitude: 7.4951,
});

// First route (A → B, OSRM-shaped response)
const trip = await client.route({
  originLat: 9.0579,
  originLng: 7.4951,
  destLat: 9.0765,
  destLng: 7.3986,
  mode: "drive",
});

MaimapsClient

The single entry point. Construct it once with your API key and reuse it; it holds no per-request state and is safe to share across your app.

new MaimapsClient(options)

Signature
new MaimapsClient(options: MaimapsClientOptions)

interface MaimapsClientOptions {
  apiKey: string;
  environment?: 'staging' | 'production';
  apiBaseUrl?: string;
  mapBaseUrl?: string;
  fetch?: FetchLike;
  timeoutMs?: number;
}

Creates a client. Staging hosts are baked in, so during development apiKey is the only option you need.

Parameters

apiKeyrequired
string
Your MAIMAPS key (mk_live_… or mk_test_…). Throws if empty.
environmentoptional
'staging' | 'production'
Defaults to 'staging'. Passing 'production' without both apiBaseUrl and mapBaseUrl throws, because production hostnames are not published until GA.
apiBaseUrloptional
string
Override the REST gateway host.
mapBaseUrloptional
string
Override the map-display host.
fetchoptional
FetchLike
Inject a custom fetch. Useful for tests, proxies, or non-browser runtimes.
timeoutMsoptional
number
Per-attempt request timeout. Defaults to 10000.

client.search(params)

Signature
search(params: SearchParams): Promise<SearchResult[]>

interface SearchParams {
  q: string;
  latitude?: number;
  longitude?: number;
  limit?: number;
  bbox?: string;
  type?: string;
  category?: string;
}

Free-text search across places, addresses, POIs, and loccodes. Passing latitude and longitude biases results toward the user and fills in distanceKm.

Returns. An array of SearchResult, already unwrapped from the response envelope and camelCased.

client.route(params)

Signature
route(params: RouteParams): Promise<RouteResult>

interface RouteParams {
  originLat: number;
  originLng: number;
  destLat: number;
  destLng: number;
  mode?: TravelMode;   // 'drive' | 'walk'
}

Simple A-to-B routing between two coordinate pairs. Maps to GET /sdk/v1/route.

Returns. RouteResult with routes[], each carrying distance (meters), duration (seconds), geometry (encoded polyline, precision 5), and legs[].

client.routeAdvanced(params)

Signature
routeAdvanced(params: RouteAdvancedParams): Promise<RouteResult>

interface RouteAdvancedParams {
  originLat?: number;
  originLng?: number;
  originLoccode?: string;
  destLat?: number;
  destLng?: number;
  destLoccode?: string;
  waypoints?: RouteWaypointInput[];   // { order, latitude, longitude }
  mode?: TravelMode;
}

Routing with ordered waypoints, and with loccodes accepted in place of coordinates at either end. Maps to POST /sdk/v1/route. Give each endpoint either a coordinate pair or a loccode, not neither.

Returns. The same RouteResult shape as route().

client.reverseGeocode(params)

Signature
reverseGeocode(params: { latitude: number; longitude: number }): Promise<ReverseGeocodeResult>

Turn a coordinate into an address plus the loccode of the street it sits on.

Returns. ReverseGeocodeResult with address, street, houseNumber, city, state, country, and loccode.

client.pointDetails(params)

Signature
pointDetails(params: PointDetailsParams): Promise<PointDetails>

interface PointDetailsParams {
  latitude: number;
  longitude: number;
  osmId?: string;
  osmType?: string;     // 'n' | 'w' | 'r'
  searchName?: string;
  searchAddress?: string;
}

Resolve a tapped point or dropped pin into a full place record plus its loccode and a share link. Pass osmId and osmType from a search result to pin the lookup to that exact feature.

Returns. PointDetails with a nested place and loccode.

client.styleUrl(params?)

Signature
styleUrl(params?: { family?: string; mode?: 'light' | 'dark' }): string

Build the keyed style.json URL on the map host. Hand this to MapLibre as its style. Each fetch of this URL counts as one billed map load.

Returns. An absolute URL with your key already appended.

client.transformMapResource(url)

Signature
transformMapResource(url: string): string

Append your key to a map-host resource URL. Wire this into MapLibre's transformRequest so tiles, sprites, and glyphs authenticate. URLs that are not map-host resources pass through untouched.

Returns. The URL, keyed if it targets the map host.

Loccodes & categories

Two namespaces hang off the client for the endpoints that are not core map operations.

client.loccode.resolve(code)

Signature
client.loccode.resolve(code: string): Promise<LoccodeResult>

Expand a loccode into its street geometry and metadata. Remember a loccode is a street: you get a LineString plus a representativePoint midpoint.

Returns. LoccodeResult with geometry, representativePoint, streetName, state, lga, valid, and coverage.

client.loccode.nearest(params)

Signature
client.loccode.nearest(params: { latitude: number; longitude: number }): Promise<LoccodeResult>

Find the loccode of the street nearest a coordinate. The inverse of resolve, and the fastest way to turn a GPS fix into a Maiaddy address.

Returns. The same LoccodeResult shape as resolve().

client.placeCategories.list()

Signature
client.placeCategories.list(): Promise<PlaceCategory[]>

The category taxonomy. Fetch once at startup and cache: these keys are what search accepts in its category filter. Never billed against your quota.

Returns. An array of PlaceCategory with key, label, and optional parentKey.

Errors

Every failure throws a subclass of MaimapsError, so you can branch on the type instead of parsing status codes. Idempotent GETs are retried automatically, honoring Retry-After; a POST is never retried for you.

MaimapsError

Signature
class MaimapsError extends Error {
  readonly httpStatus?: number;
  readonly code?: string;
}

Base class for everything the SDK throws. Anything the SDK does not map to a specific subclass, including the navigation service's UPPER_SNAKE_CASE codes such as ROUTING_FAILED and ENGINE_UNAVAILABLE, arrives as this with code and httpStatus intact.

InvalidApiKeyError

Signature
class InvalidApiKeyError extends MaimapsError {}   // 401 invalid_api_key

Missing, malformed, revoked, or wrong-product key. Not retryable: fix the key.

RateLimitError

Signature
class RateLimitError extends MaimapsError {
  readonly retryAfterSeconds?: number;   // parsed from the Retry-After header
}   // 429 rate_limited

Per-key rate limit hit. The SDK already retried the request if it was an idempotent GET, so seeing this means the retries were also exhausted. Back off for retryAfterSeconds.

QuotaExceededError

Signature
class QuotaExceededError extends MaimapsError {}   // 429 quota_exceeded

The monthly cap for this dimension is spent. Retrying will not help until the cap resets or the plan changes. Distinct from RateLimitError, which is transient.

ServerError

Signature
class ServerError extends MaimapsError {}   // any 5xx, incl. 503 auth_unavailable

Something failed upstream. Safe to retry an idempotent call after a backoff.

NetworkError

Signature
class NetworkError extends MaimapsError {}

The request never got an HTTP response: DNS failure, offline, timeout. There is no httpStatus on this one.

Utilities

Standalone functions, importable without constructing a client.

decodePolyline(encoded, precision?)

Signature
decodePolyline(encoded: string, precision = 5): Array<[number, number]>

Decode an encoded route polyline. Returns [latitude, longitude] pairs. The map components want [longitude, latitude], so flip before rendering: .map(([lat, lng]) => [lng, lat]).

Returns. An array of [lat, lng] tuples.

Wordmark

The map components render the Maimaps wordmark for you, so most apps never touch these. Reach for them only when you draw the map surface yourself, for example on a custom canvas or a server-rendered static image, and still need a correctly sized, correctly placed lockup.

resolveWordmark(options)

Signature
resolveWordmark(options: {
  viewport: WordmarkViewport;
  manifest?: WordmarkManifest;
  layout?: WordmarkLayout;
}): ResolvedWordmark

Pick the right wordmark asset and geometry for a given map viewport. Falls back to the art bundled inside the SDK when no manifest is passed, so it works offline and under a restrictive CSP.

Returns. A ResolvedWordmark carrying the asset to draw plus its resolved size and position.

fetchWordmarkManifest(options)

Signature
fetchWordmarkManifest(options: {
  mapBaseUrl: string;
  apiKey: string;
  fetch?: FetchLike;
}): Promise<WordmarkManifest>

Fetch the current wordmark manifest from /sdk/wordmark.json to pick up updated art. Never billed. If it rejects, keep using the bundled art rather than dropping the wordmark, which is a licence violation.

Returns. The manifest describing the available wordmark assets.

wordmarkManifestUrl(mapBaseUrl, apiKey)

Signature
wordmarkManifestUrl(mapBaseUrl: string, apiKey: string): string

Build the keyed /sdk/wordmark.json URL without fetching it, for when you want to run the request through your own HTTP stack.

Returns. The fully-qualified manifest URL with the key appended.

wordmarkScaleFor(viewport, layout?)

Signature
wordmarkScaleFor(viewport: WordmarkViewport, layout?: WordmarkLayout): number

The scale factor the lockup should use at a given viewport size. The wordmark scales with map width and is clamped so it stays whole and inside the map bounds on everything from wide banners to thumbnails.

Returns. A multiplier to apply to the asset's intrinsic size.

DEFAULT_WORDMARK_LAYOUT

Signature
const DEFAULT_WORDMARK_LAYOUT: WordmarkLayout

The layout the SDKs use unless you override it: bottom-left, with the standard edge padding and scaling curve.

Resources

This SDK is 0.1.0 and in beta. Breaking changes can land in minor versions until 1.0.0, so pin the version if you need stability.