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
npm install @maimaps/jsRequires 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.
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)
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
apiKeyrequiredenvironmentoptionalapiBaseUrloptionalmapBaseUrloptionalfetchoptionaltimeoutMsoptionalclient.search(params)
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)
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)
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)
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)
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?)
styleUrl(params?: { family?: string; mode?: 'light' | 'dark' }): stringBuild 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)
transformMapResource(url: string): stringAppend 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)
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)
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()
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
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
class InvalidApiKeyError extends MaimapsError {} // 401 invalid_api_keyMissing, malformed, revoked, or wrong-product key. Not retryable: fix the key.
RateLimitError
class RateLimitError extends MaimapsError {
readonly retryAfterSeconds?: number; // parsed from the Retry-After header
} // 429 rate_limitedPer-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
class QuotaExceededError extends MaimapsError {} // 429 quota_exceededThe 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
class ServerError extends MaimapsError {} // any 5xx, incl. 503 auth_unavailableSomething failed upstream. Safe to retry an idempotent call after a backoff.
NetworkError
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?)
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)
resolveWordmark(options: {
viewport: WordmarkViewport;
manifest?: WordmarkManifest;
layout?: WordmarkLayout;
}): ResolvedWordmarkPick 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)
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)
wordmarkManifestUrl(mapBaseUrl: string, apiKey: string): stringBuild 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?)
wordmarkScaleFor(viewport: WordmarkViewport, layout?: WordmarkLayout): numberThe 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
const DEFAULT_WORDMARK_LAYOUT: WordmarkLayoutThe 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.