From 58f39da010aeae764c0567eb7c8c00824e2fecb9 Mon Sep 17 00:00:00 2001 From: justrouting Date: Thu, 8 Oct 2026 16:17:15 +0000 Subject: [PATCH] refine sdk doc --- docs.json | 12 +- guides/sdks.mdx | 100 -------------- index.mdx | 2 +- mcp/overview.mdx | 2 +- sdk/go-client.mdx | 344 ++++++++++++++++++++++++++++++++++++++++++++++ sdk/js-client.mdx | 330 ++++++++++++++++++++++++++++++++++++++++++++ sdk/overview.mdx | 31 +++++ sdk/py-client.mdx | 337 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1054 insertions(+), 104 deletions(-) delete mode 100644 guides/sdks.mdx create mode 100644 sdk/go-client.mdx create mode 100644 sdk/js-client.mdx create mode 100644 sdk/overview.mdx create mode 100644 sdk/py-client.mdx diff --git a/docs.json b/docs.json index 2ae180f..2912173 100644 --- a/docs.json +++ b/docs.json @@ -37,6 +37,15 @@ "api-reference/fleet-optimization" ] }, + { + "group": "SDK", + "pages": [ + "sdk/overview", + "sdk/py-client", + "sdk/go-client", + "sdk/js-client" + ] + }, { "group": "MCP", "pages": [ @@ -64,8 +73,7 @@ "group": "Guides", "pages": [ "guides/draw-route-on-map", - "guides/turn-by-turn", - "guides/sdks" + "guides/turn-by-turn" ] } ] diff --git a/guides/sdks.mdx b/guides/sdks.mdx deleted file mode 100644 index f9cefff..0000000 --- a/guides/sdks.mdx +++ /dev/null @@ -1,100 +0,0 @@ ---- -title: "SDKs" -description: "Official JustRouting client libraries for Python, JavaScript/TypeScript, and Go — zero dependencies." -icon: "cube" ---- - -## Overview - -SDKs are official clients, all **zero third-party dependencies** and open source (MIT). They cover the all API families — Directions, Distance Matrix, Map Matching, Trip, Nearest, Geocode, Fleet Optimization — with typed errors and built-in 429 retries. - -| Language | Package | Install | Source | -| --- | --- | --- | --- | -| Python 3.9+ | `justrouting` | `pip install justrouting` | [py-client](https://github.com/justrouting/py-client) | -| JavaScript / TypeScript | `@justrouting/client` | `npm install @justrouting/client` | [js-client](https://github.com/justrouting/js-client) | -| Go | `github.com/justrouting/go-client` | `go get github.com/justrouting/go-client` | [go-client](https://github.com/justrouting/go-client) | - -## Shared conventions - -- `new Client(apiKey)` attaches `Authorization: Bearer` to every request automatically -- Coordinates always use `[lng, lat]` order -- Unreachable matrix cells come back as `null` (not 0) — you decide how to handle them -- Zero dependencies means they run on Lambda, Cloudflare Workers, and edge environments as-is - -## Python example - -```python -import justrouting - -client = justrouting.Client("YOUR_API_KEY") - -# Directions -route = client.routes.get(justrouting.RouteRequest( - origin=[103.708362, 1.357371], - destination=[103.984748, 1.352212], - overview="full", -)) - -print(f"Distance: {route.distance / 1000:.1f} km") -print(f"Duration: {route.duration / 60:.0f} min") -``` - -## JavaScript example - -```ts -import { Client } from '@justrouting/client'; - -const client = new Client('YOUR_API_KEY', { timeout: 10 }); - -const route = await client.routes.get({ - origin: [103.708362, 1.357371], - destination: [103.984748, 1.352212], - overview: "full", -}); - -console.log(`Distance: ${(route.distance / 1000).toFixed(1)} km`); -console.log(`Duration: ${Math.round(route.duration / 60)} min`); -``` - -## Go example - -```go -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "time" - - justrouting "github.com/justrouting/go-client" -) - -func main() { - client := justrouting.NewClient("YOUR_API_KEY", - justrouting.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}), - ) - - route, err := client.Routes.Get(context.Background(), &justrouting.RouteRequest{ - Origin: []float64{103.708362, 1.357371}, - Destination: []float64{103.984748, 1.352212}, - Overview: "full", - }) - if err != nil { - log.Fatal(err) - } - - fmt.Printf("Distance: %.1f km\n", route.Distance/1000) - fmt.Printf("Duration: %.0f min\n", route.Duration/60) -} -``` - -## No SDK? No problem - -The SDKs are thin wrappers over plain REST. You can always call the HTTP endpoints directly — every API Reference page has curl examples. - -## Related - -- [Quickstart](/quickstart) -- [Authentication](/authentication) diff --git a/index.mdx b/index.mdx index d4b2470..6d1ca3a 100644 --- a/index.mdx +++ b/index.mdx @@ -79,7 +79,7 @@ curl "https://api.justrouting.tech/route/v1/driving/103.708362,1.357371;103.9847 } ``` -[Get a free API key](https://justrouting.tech/signup) · [Read the Quickstart](/quickstart) · [Explore the SDKs](/guides/sdks) +[Get a free API key](https://justrouting.tech/signup) · [Read the Quickstart](/quickstart) · [Explore the SDKs](/sdk/overview) ## Built for AI agents diff --git a/mcp/overview.mdx b/mcp/overview.mdx index 127335a..43f93e6 100644 --- a/mcp/overview.mdx +++ b/mcp/overview.mdx @@ -262,5 +262,5 @@ Once connected, Cursor can use the JustRouting tools directly. * [Directions API](https://docs.justrouting.tech/api/directions) * [Distance Matrix API](https://docs.justrouting.tech/api/matrix) * [Fleet Optimization API](https://docs.justrouting.tech/api/optimization) -* [SDKs](https://docs.justrouting.tech/sdks) +* [SDKs](https://docs.justrouting.tech/sdk/overview) * [JustRouting MCP on GitHub](https://github.com/justrouting/mcp) diff --git a/sdk/go-client.mdx b/sdk/go-client.mdx new file mode 100644 index 0000000..8071e0e --- /dev/null +++ b/sdk/go-client.mdx @@ -0,0 +1,344 @@ +--- +title: "Go Client" +description: "Official Go client for the JustRouting API — zero dependencies, typed errors, and built-in retries." +icon: "golang" +sidebarTitle: "Go-Client" +--- + +## Overview + +Go client for the [JustRouting](https://justrouting.tech) API — routing, distance matrices, map matching, trips, nearest-road lookup, vehicle routing optimization, and geocoding across Southeast Asia. + +No dependencies outside the standard library. + +## Install + +```shell +go get github.com/justrouting/go-client +``` + +```go +import justrouting "github.com/justrouting/go-client" +``` + +## Quickstart + +```go +package main + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "github.com/justrouting/go-client" +) + +func main() { + client := justrouting.NewClient( + "YOUR_API_KEY", + justrouting.WithHTTPClient(&http.Client{ + Timeout: 10 * time.Second, + }), + ) + + route, err := client.Routes.Get( + context.Background(), + &justrouting.RouteRequest{ + Origin: []float64{103.708362, 1.357371}, + Destination: []float64{103.984748, 1.352212}, + }, + ) + + if err != nil { + log.Fatal(err) + } + + fmt.Printf( + "Distance: %.1f km\n", + route.Distance/1000, + ) +} +``` + +> The coordinates above are in Singapore. See [Coordinates must share a country](#coordinates-must-share-a-country). + +## Services + +A `Client` exposes eight services. + +### Routes + +`Routes.Get` returns the best route. `Routes.GetAll` additionally returns alternatives and the snapped input waypoints. + +```go +route, err := client.Routes.Get(ctx, &justrouting.RouteRequest{ + Origin: justrouting.Point{103.8198, 1.3521}, + Destination: justrouting.Point{103.9915, 1.3644}, + Waypoints: []justrouting.Point{{103.8514, 1.2897}}, // stops in order + Overview: "full", // full geometry + Steps: true, // turn-by-turn +}) + +fmt.Println(route.Distance) // metres +fmt.Println(route.Duration) // seconds +``` + +`Route.Geometry` holds whichever encoding you asked for: + +```go +polyline, err := route.Geometry.Polyline() // default, and "polyline6" +line, err := route.Geometry.GeoJSON() // when Geometries: "geojson" +``` + +### Matrix + +Travel time and distance between many points at once. + +```go +m, err := client.Matrix.Get(ctx, &justrouting.MatrixRequest{ + Coordinates: []justrouting.Point{depot, stopA, stopB}, + Sources: []int{0}, // only the depot row; cheaper than N×N + Destinations: []int{1, 2}, +}) + +if seconds, ok := m.Duration(0, 1); ok { + fmt.Printf("depot → stopA: %.0f min\n", seconds/60) +} +``` + +The accessors return `false` for unreachable pairs. The API reports those as `null`, which is deliberately kept distinct from a genuine zero — that is why `Durations` and `Distances` hold `*float64`. + +### Map Matching + +Snap a noisy GPS trace onto the road network and get the route that was driven. + +```go +match, err := client.MapMatching.Get(ctx, &justrouting.MapMatchingRequest{ + Coordinates: trace, // GPS points in chronological order, at least 2 +}) +fmt.Printf("%.0f%% confidence\n", match.Confidence*100) +fmt.Println(match.Distance) // metres, via the embedded Route +``` + +`Match` embeds `Route`, so every route field (`Distance`, `Duration`, `Geometry`, `Legs`, …) is promoted, plus the engine's `Confidence` (0–1). `MapMatching.GetAll` additionally returns `Tracepoints` — the input coordinates snapped to the road network, aligned with your input; an entry is `nil` when the engine could not match that point. Useful options: `Timestamps` (UNIX seconds per point), `Radiuses` (max snap distance, one value per point — the default is only a few metres, so noisy GPS points usually need it), `Gaps` (`"split"`/`"ignore"`), `Tidy`, `Waypoints` (indices to use as waypoints), and `Snapping` (`"default"`/`"any"`). + +### Trip + +Visit a set of points in the fastest possible order (a travelling-salesman heuristic). + +```go +trip, err := client.Trip.Get(ctx, &justrouting.TripRequest{ + Coordinates: []justrouting.Point{depot, stopA, stopB}, +}) +fmt.Println(trip.Distance) // metres +``` + +By default the trip returns to its starting point; pass `Roundtrip` (a `bool` pointer) to change that, and `Source`/`Destination` (`"any"`, `"first"`/`"last"`) to pin the ends. `Trip.GetAll` also returns `Waypoints` in the order the trip visits them. + +### Nearest + +Find the road segment closest to a coordinate. + +```go +wp, err := client.Nearest.Get(ctx, &justrouting.NearestRequest{ + Coordinate: []float64{103.8198, 1.3521}, +}) +fmt.Printf("%s, %.0f m away\n", wp.Name, wp.Distance) +``` + +Pass `Number` to get the second-, third-, … nearest segments; `Nearest.GetAll` returns all of them. The returned `Waypoint` includes the OSM `Nodes` of the matched segment. + +### Geocode + +`Geocode.Search` converts an address into coordinates, by free text or by structured fields (exactly one of the two): + +```go +results, err := client.Geocode.Search(ctx, &justrouting.GeocodeRequest{ + Text: "Marina Bay Sands, Singapore", +}) +for _, r := range results.Results { + fmt.Println(r.Formatted, r.Location()) // address text, [lon, lat] +} +``` + +```go +results, err := client.Geocode.Search(ctx, &justrouting.GeocodeRequest{ + Structured: &justrouting.StructuredQuery{ + Housenumber: "10", + Street: "Bayfront Avenue", + City: "Singapore", + }, +}) +``` + +Results are ordered best first; an empty `Results` list simply means nothing matched. The client always requests `format=json`, regardless of the API's default response format. Use `Filters` (repeatable, e.g. `countrycode:sg`) to restrict results and `Bias` (e.g. `proximity:103.8,1.3`) to prefer places near a point. `GeocodeResult.Location()` returns a `Point` in the usual `[longitude, latitude]` order, ready to feed into `Routes`, `Matrix`, or `Optimization`. Errors from the geocoding upstream are classified by HTTP status like any other failure (`401` → `ErrUnauthorized`, `429` → `ErrRateLimited`, `502` → `ErrUpstreamUnavailable`). + +### Optimization + +Assign tasks to a fleet and order each vehicle's stops. + +```go +solution, err := client.Optimization.Solve(ctx, &justrouting.OptimizationRequest{ + Vehicles: []justrouting.Vehicle{ + {ID: 1, Start: depot, End: depot, Capacity: []int{4}}, + }, + Jobs: []justrouting.Job{ + {ID: 1, Location: stopA, Delivery: []int{1}, Service: 300}, + {ID: 2, Location: stopB, Delivery: []int{2}, Service: 300}, + }, +}) + +for _, route := range solution.Routes { + fmt.Printf("vehicle %d: %d stops\n", route.Vehicle, len(route.Steps)) +} +fmt.Println(len(solution.Unassigned), "task(s) could not be served") +``` + +Use `Shipments` instead of `Jobs` for pickup-and-delivery pairs that must be served in order by the same vehicle. + +### Health + +The only call that works without an API key, which makes it a useful connectivity check. + +```go +health, err := client.Health.Get(ctx) +fmt.Println(health.OK(), health.Upstreams) +``` + +## Error handling + +Every failure is an `*Error`. Classify it with `errors.Is` against the package sentinels rather than matching on message text: + +```go +switch { +case errors.Is(err, justrouting.ErrQuotaExceeded): + // daily allowance used up — retrying will not help +case errors.Is(err, justrouting.ErrRateLimited): + // throttled; the client already retried +case errors.Is(err, justrouting.ErrCrossCountry): + // coordinates span more than one country +case errors.Is(err, justrouting.ErrNoRoute): + // no road connects these points +} +``` + +| Sentinel | Meaning | +| --- | --- | +| `ErrUnauthorized` | API key missing, invalid, or revoked | +| `ErrRateLimited` | Throttled (per-second limit or daily quota) | +| `ErrQuotaExceeded` | Daily quota exhausted; retrying will not help | +| `ErrPlanLimitExceeded` | Too many matrix coordinates, jobs, or vehicles | +| `ErrCrossCountry` | Coordinates span more than one country | +| `ErrInvalidCoordinates` | Coordinate malformed or out of range | +| `ErrNoRoute` | No route exists between the points | +| `ErrUpstreamUnavailable` | Routing engine unreachable; usually transient | +| `ErrInvalidRequest` | Rejected locally before any request was sent | + +Reach for the concrete type when you need the status code or raw body: + +```go +var apiErr *justrouting.Error +if errors.As(err, &apiErr) { + log.Printf("HTTP %d: %s\n%s", apiErr.StatusCode, apiErr.Message, apiErr.Body) +} +``` + +## Configuration + +| Option | Default | Purpose | +| --- | --- | --- | +| `WithHTTPClient` | 30s timeout | Custom transport, timeouts, instrumentation | +| `WithBaseURL` | `https://api.justrouting.tech` | Target a local or staging server | +| `WithUserAgent` | `justrouting-go/` | Identify your application | +| `WithMaxRetries` | `2` | Retry budget on top of the initial attempt | +| `WithBackoff` | 500ms → 8s, jittered | Replace the retry delay schedule | + +`NewClient` never returns an error. If an option is invalid, the failure is retained and returned by the first API call, so a misconfigured client fails loudly at the point of use instead of silently falling back to a default. + +### Retries + +Rate limits (429), server errors (5xx), and transport failures are retried with exponential backoff and jitter; a `Retry-After` header takes precedence when present. Other 4xx responses are returned immediately — they would fail identically on a retry and would still consume quota. + +Retries are bounded by the context, not the HTTP client's timeout. A timeout on your `http.Client` applies to each individual attempt; use a context deadline to bound the whole call: + +```go +ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) +defer cancel() +``` + +## Things to know + +### Coordinates are `[longitude, latitude]` + +This is the GeoJSON order, and the reverse of the "lat, lng" used by most map UIs. Swapped coordinates are usually caught locally — a longitude in the latitude slot fails the `[-90, 90]` check before a request is sent — but a swap that stays in range will silently route somewhere unexpected. + +`Point` is defined over `[]float64`, so both of these work: + +```go +Origin: []float64{103.8198, 1.3521} +Origin: justrouting.Point{103.8198, 1.3521} +``` + +### Coordinates must share a country + +Every coordinate in a single request must fall within one country; the API routes each request to a per-country engine. A Singapore → Kuala Lumpur request fails with `ErrCrossCountry`. + +Supported countries: Brunei, Cambodia, Indonesia, Laos, Malaysia, Myanmar, the Philippines, Singapore, Thailand, and Vietnam. + +### Plan limits + +| | Free | Hobby | +| --- | --- | --- | +| Requests per day | 100 | 10,000 | +| Requests per second | 5 | 10 | +| Matrix coordinates | 100 | 500 | +| Jobs per optimization | 100 | 1,000 | +| Vehicles per optimization | 10 | 50 | + +Exceeding a size limit returns `ErrPlanLimitExceeded`; exhausting the daily allowance returns `ErrQuotaExceeded`. + +## Examples + +Runnable programs live in [`examples/`](https://github.com/justrouting/go-client/tree/main/examples): + +```shell +export JUSTROUTING_API_KEY= +go run ./examples/route +go run ./examples/matrix +go run ./examples/matching +go run ./examples/trip +go run ./examples/nearest +go run ./examples/geocode +go run ./examples/optimization +``` + +## Development + +```shell +go build ./... +go vet ./... +go test -race -cover ./... +``` + +The default suite runs entirely against `httptest` servers — no network access and no API key. + +Integration tests hit a live API and are behind a build tag, so they never run by accident: + +```shell +go test -tags=integration ./... # health and auth only +JUSTROUTING_API_KEY= go test -tags=integration ./... # full coverage +JUSTROUTING_BASE_URL=http://localhost:8080 go test -tags=integration ./... +``` + +## License + +[MIT](https://github.com/justrouting/go-client/blob/main/LICENSE) + +## Related + +- [SDK Overview](/sdk/overview) +- [Python Client](/sdk/py-client) +- [JavaScript Client](/sdk/js-client) diff --git a/sdk/js-client.mdx b/sdk/js-client.mdx new file mode 100644 index 0000000..eb85cae --- /dev/null +++ b/sdk/js-client.mdx @@ -0,0 +1,330 @@ +--- +title: "JavaScript Client" +description: "Official JavaScript/TypeScript client for the JustRouting API — zero dependencies, typed errors, and built-in retries. Requires Node 20.3+." +icon: "js" +sidebarTitle: "Js-Client" +--- + +## Overview + +JavaScript client for the [JustRouting](https://justrouting.tech) API — routing, distance matrices, map matching, trips, nearest-road lookup, vehicle routing optimization, and geocoding across Southeast Asia. + +TypeScript-first, compiled to pure ESM with type declarations. No dependencies outside the Node.js standard library (`fetch`). Requires Node 20.3+. + +## Install + +```shell +npm install @justrouting/client +``` + +```ts +import { Client } from '@justrouting/client'; +``` + +## Quickstart + +```ts +import { Client } from '@justrouting/client'; + +const client = new Client('YOUR_API_KEY', { timeout: 10 }); + +const route = await client.routes.get({ + origin: [103.708362, 1.357371], + destination: [103.984748, 1.352212], +}); + +console.log(`Distance: ${(route.distance / 1000).toFixed(1)} km`); +``` + +> The coordinates above are in Singapore. See [Coordinates must share a country](#coordinates-must-share-a-country). + +## Services + +A `Client` exposes eight services. + +### Routes + +`routes.get` returns the best route. `routes.getAll` additionally returns alternatives and the snapped input waypoints. + +```ts +const route = await client.routes.get({ + origin: [103.8198, 1.3521], + destination: [103.9915, 1.3644], + waypoints: [[103.8514, 1.2897]], // stops in order + overview: 'full', // full geometry + steps: true, // turn-by-turn +}); + +console.log(route.distance); // metres +console.log(route.duration); // seconds +``` + +`route.geometry` holds whichever encoding you asked for: + +```ts +const polyline = route.geometry.polyline(); // default, and "polyline6" +const line = route.geometry.geoJSON(); // when geometries: "geojson" +``` + +### Matrix + +Travel time and distance between many points at once. + +```ts +const m = await client.matrix.get({ + coordinates: [depot, stopA, stopB], + sources: [0], // only the depot row; cheaper than N×N + destinations: [1, 2], +}); + +const seconds = m.duration(0, 1); +if (seconds !== null) { + console.log(`depot → stopA: ${Math.round(seconds / 60)} min`); +} +``` + +The accessors return `null` for unreachable pairs. The API reports those as `null`, which is deliberately kept distinct from a genuine zero — that is why `durations` and `distances` hold `number | null` entries. + +### Map Matching + +Snap a noisy GPS trace onto the road network and get the route that was driven. + +```ts +const match = await client.mapMatching.get({ + coordinates: trace, // GPS points in chronological order, at least 2 +}); +console.log(`${Math.round(match.confidence * 100)}% confidence`); +console.log(match.distance); // metres, via the embedded Route +``` + +`Match` extends `Route`, so every route field (`distance`, `duration`, `geometry`, `legs`, …) is available, plus the engine's `confidence` (0–1). `mapMatching.getAll` additionally returns `tracepoints` — the input coordinates snapped to the road network, aligned with your input; an entry is `null` when the engine could not match that point. Useful options: `timestamps` (UNIX seconds per point), `radiuses` (max snap distance, one value per point — the default is only a few metres, so noisy GPS points usually need it), `gaps` (`"split"`/`"ignore"`), `tidy`, `waypoints` (indices to use as waypoints), and `snapping` (`"default"`/`"any"`). + +### Trip + +Visit a set of points in the fastest possible order (a travelling-salesman heuristic). + +```ts +const trip = await client.trip.get({ + coordinates: [depot, stopA, stopB], +}); +console.log(trip.distance); // metres +``` + +By default the trip returns to its starting point; set `roundtrip: false` to change that, and `source`/`destination` (`"any"`, `"first"`/`"last"`) to pin the ends. `trip.getAll` also returns `waypoints` in the order the trip visits them. + +### Nearest + +Find the road segment closest to a coordinate. + +```ts +const wp = await client.nearest.get({ + coordinate: [103.8198, 1.3521], +}); +console.log(`${wp.name}, ${Math.round(wp.distance)} m away`); +``` + +Pass `number` to get the second-, third-, … nearest segments; `nearest.getAll` returns all of them. The returned `Waypoint` includes the OSM `nodes` of the matched segment. + +### Geocode + +`geocode.search` converts an address into coordinates, by free text or by structured fields (exactly one of the two): + +```ts +const resp = await client.geocode.search({ + text: 'Marina Bay Sands, Singapore', +}); +for (const r of resp.results) { + console.log(r.formatted, r.location()); // address text, [lon, lat] +} +``` + +```ts +const resp = await client.geocode.search({ + structured: { + housenumber: '10', + street: 'Bayfront Avenue', + city: 'Singapore', + }, +}); +``` + +Results are ordered best first; an empty `results` list simply means nothing matched. The client always requests `format=json`, regardless of the API's default response format. Use `filters` (repeatable, e.g. `countrycode:sg`) to restrict results and `bias` (e.g. `proximity:103.8,1.3`) to prefer places near a point. `GeocodeResult.location()` returns a `Point` in the usual `[longitude, latitude]` order, ready to feed into `routes`, `matrix`, or `optimization`. Errors from the geocoding upstream are classified by HTTP status like any other failure (`401` → `UnauthorizedError`, `429` → `RateLimitedError`, `502` → `UpstreamUnavailableError`). + +### Optimization + +Assign tasks to a fleet and order each vehicle's stops. + +```ts +const solution = await client.optimization.solve({ + vehicles: [ + { id: 1, start: depot, end: depot, capacity: [4] }, + ], + jobs: [ + { id: 1, location: stopA, delivery: [1], service: 300 }, + { id: 2, location: stopB, delivery: [2], service: 300 }, + ], +}); + +for (const route of solution.routes) { + console.log(`vehicle ${route.vehicle}: ${route.steps.length} stops`); +} +console.log(`${solution.unassigned.length} task(s) could not be served`); +``` + +Use `shipments` instead of `jobs` for pickup-and-delivery pairs that must be served in order by the same vehicle. + +### Health + +The only call that works without an API key, which makes it a useful connectivity check. + +```ts +const health = await client.health.get(); +console.log(health.ok(), health.upstreams); +``` + +## Error handling + +Every API failure throws an `ApiError` subclass. Classify it with `instanceof` rather than matching on message text: + +```ts +try { + const route = await client.routes.get(req); +} catch (err) { + if (err instanceof QuotaExceededError) { + // daily allowance used up — retrying will not help + } else if (err instanceof RateLimitedError) { + // throttled; the client already retried + } else if (err instanceof CrossCountryError) { + // coordinates span more than one country + } else if (err instanceof NoRouteError) { + // no road connects these points + } +} +``` + +| Class | Meaning | +| --- | --- | +| `UnauthorizedError` | API key missing, invalid, or revoked | +| `RateLimitedError` | Throttled (per-second limit or daily quota) | +| `QuotaExceededError` | Daily quota exhausted; retrying will not help (subclass of `RateLimitedError`) | +| `PlanLimitExceededError` | Too many matrix coordinates, jobs, or vehicles | +| `CrossCountryError` | Coordinates span more than one country | +| `InvalidCoordinatesError` | Coordinate malformed or out of range | +| `NoRouteError` | No route exists between the points | +| `UpstreamUnavailableError` | Routing engine unreachable; usually transient | +| `InvalidRequestError` | Rejected locally before any request was sent | +| `TransportError` | Network-level failure; transient and retried | +| `DecodeError` | A successful response could not be decoded | + +Reach for `ApiError` when you need the status code, engine code, or raw body: + +```ts +} catch (err) { + if (err instanceof ApiError) { + console.log(`HTTP ${err.statusCode}: ${err.message}\n${err.body}`); + } +} +``` + +> The Python client names this class `Error`; here it is `ApiError`, because JavaScript already has a global `Error` and an exported class of the same name would shadow it. + +## Configuration + +| Option | Default | Purpose | +| --- | --- | --- | +| `baseUrl` | `https://api.justrouting.tech` | Target a local or staging server | +| `userAgent` | `justrouting-js/` | Identify your application | +| `maxRetries` | `2` | Retry budget on top of the initial attempt | +| `backoff` | 500ms → 8s, jittered | Replace the retry delay schedule | +| `timeout` | `30` | Seconds per HTTP attempt; `null` disables it | +| `fetch` | global `fetch` | Inject a fetch implementation, for tests or instrumentation | + +Invalid options throw a `TypeError` immediately. + +### Cancellation and deadlines + +Every call accepts an options object with a `signal` and a `timeout`: + +```ts +const controller = new AbortController(); +setTimeout(() => controller.abort(), 1000); // give up after 1s + +const route = await client.routes.get(req, { + signal: controller.signal, + timeout: 30, // seconds for the whole call, retries included +}); +``` + +The per-call `timeout` bounds the whole retry sequence, like a context deadline in the Go client; the constructor `timeout` applies to each individual attempt. When the deadline expires the call rejects with a `DOMException` named `TimeoutError`; aborting the signal propagates the abort reason unchanged. + +### Retries + +Rate limits (429), server errors (5xx), and transport failures are retried with exponential backoff and jitter; a `Retry-After` header takes precedence when present. Other 4xx responses are returned immediately — they would fail identically on a retry and would still consume quota. + +## Things to know + +### Coordinates are `[longitude, latitude]` + +This is the GeoJSON order, and the reverse of the "lat, lng" used by most map UIs. Swapped coordinates are usually caught locally — a longitude in the latitude slot fails the `[-90, 90]` check before a request is sent — but a swap that stays in range will silently route somewhere unexpected. + +### Coordinates must share a country + +Every coordinate in a single request must fall within one country; the API routes each request to a per-country engine. A Singapore → Kuala Lumpur request fails with `CrossCountryError`. + +Supported countries: Brunei, Cambodia, Indonesia, Laos, Malaysia, Myanmar, the Philippines, Singapore, Thailand, and Vietnam. + +### Plan limits + +| | Free | Hobby | +| --- | --- | --- | +| Requests per day | 100 | 10,000 | +| Requests per second | 5 | 10 | +| Matrix coordinates | 100 | 500 | +| Jobs per optimization | 100 | 1,000 | +| Vehicles per optimization | 10 | 50 | + +Exceeding a size limit returns `PlanLimitExceededError`; exhausting the daily allowance returns `QuotaExceededError`. + +## Examples + +Runnable programs live in [`examples/`](https://github.com/justrouting/js-client/tree/main/examples): + +```shell +npm run build +export JUSTROUTING_API_KEY= +node examples/route.mjs +node examples/matrix.mjs +node examples/matching.mjs +node examples/trip.mjs +node examples/nearest.mjs +node examples/geocode.mjs +node examples/optimization.mjs +``` + +## Development + +```shell +npm install +npm run typecheck +npm run build +npm test +``` + +The default suite runs entirely against a mocked `fetch` — no network access and no API key. + +Integration tests hit a live API and are skipped unless an API key is set: + +```shell +JUSTROUTING_API_KEY= npx vitest run test/integration.test.ts +JUSTROUTING_API_KEY= JUSTROUTING_BASE_URL=http://localhost:8080 npx vitest run test/integration.test.ts +``` + +## License + +[MIT](https://github.com/justrouting/js-client/blob/main/LICENSE) + +## Related + +- [SDK Overview](/sdk/overview) +- [Python Client](/sdk/py-client) +- [Go Client](/sdk/go-client) diff --git a/sdk/overview.mdx b/sdk/overview.mdx new file mode 100644 index 0000000..ddb73dc --- /dev/null +++ b/sdk/overview.mdx @@ -0,0 +1,31 @@ +--- +title: "Overview" +description: "Official JustRouting client libraries for Python, JavaScript/TypeScript, and Go — zero dependencies." +icon: "cube" +--- + +## JustRouting SDK + +Official client libraries for every JustRouting API family — Directions, Distance Matrix, Map Matching, Trip, Nearest, Geocode, and Fleet Optimization. All SDKs are open source (MIT) and **zero third-party dependencies**, so they run on Lambda, Cloudflare Workers, and edge environments as-is, with typed errors and built-in 429 retries. + +| Language | Page | Install | Source | +| --- | --- | --- | --- | +| Python 3.9+ | [Python Client](/sdk/py-client) | `pip install justrouting` | [py-client](https://github.com/justrouting/py-client) | +| JavaScript / TypeScript | [JavaScript Client](/sdk/js-client) | `npm install @justrouting/client` | [js-client](https://github.com/justrouting/js-client) | +| Go | [Go Client](/sdk/go-client) | `go get github.com/justrouting/go-client` | [go-client](https://github.com/justrouting/go-client) | + +## Shared conventions + +- `new Client(apiKey)` attaches `Authorization: Bearer` to every request automatically +- Coordinates always use `[lng, lat]` order +- Unreachable matrix cells come back as `null` (not 0) — you decide how to handle them +- Zero dependencies means they run on Lambda, Cloudflare Workers, and edge environments as-is + +## No SDK? No problem + +The SDKs are thin wrappers over plain REST. You can always call the HTTP endpoints directly — every API Reference page has curl examples. + +## Related + +- [Quickstart](/quickstart) +- [Authentication](/authentication) diff --git a/sdk/py-client.mdx b/sdk/py-client.mdx new file mode 100644 index 0000000..2f5eabb --- /dev/null +++ b/sdk/py-client.mdx @@ -0,0 +1,337 @@ +--- +title: "Python Client" +description: "Official Python client for the JustRouting API — zero dependencies, typed errors, and built-in retries. Requires Python 3.9+." +icon: "python" +sidebarTitle: "Py-Client" +--- + +## Overview + +Python client for the [JustRouting](https://justrouting.tech) API — routing, distance matrices, geocoding, map matching, trips, nearest-road lookup, and vehicle routing optimization across Southeast Asia. + +No dependencies outside the standard library. Requires Python 3.9+. + +## Install + +```shell +pip install justrouting +``` + +> Or install from a clone of the repository: + +```shell +pip install -e . +``` + +```python +import justrouting +``` + +## Quickstart + +```python +import justrouting + +client = justrouting.Client("YOUR_API_KEY") + +route = client.routes.get( + justrouting.RouteRequest( + origin=[103.708362, 1.357371], + destination=[103.984748, 1.352212], + ), + timeout=30, +) + +print(f"Distance: {route.distance / 1000:.1f} km") +``` + +> The coordinates above are in Singapore. See [Coordinates must share a country](#coordinates-must-share-a-country). + +## Services + +A `Client` exposes eight services. + +### Routes + +`Routes.get` returns the best route. `Routes.get_all` additionally returns alternatives and the snapped input waypoints. + +```python +route = client.routes.get(justrouting.RouteRequest( + origin=[103.8198, 1.3521], + destination=[103.9915, 1.3644], + waypoints=[[103.8514, 1.2897]], # stops in order + overview="full", # full geometry + steps=True, # turn-by-turn +)) + +print(route.distance) # metres +print(route.duration) # seconds +``` + +`route.geometry` holds whichever encoding you asked for: + +```python +polyline = route.geometry.polyline() # default, and "polyline6" +line = route.geometry.geojson() # when geometries="geojson" +``` + +### Matrix + +Travel time and distance between many points at once. + +```python +m = client.matrix.get(justrouting.MatrixRequest( + coordinates=[depot, stop_a, stop_b], + sources=[0], # only the depot row; cheaper than N×N + destinations=[1, 2], +)) + +seconds = m.duration(0, 1) +if seconds is not None: + print(f"depot -> stopA: {seconds / 60:.0f} min") +``` + +The accessors return `None` for unreachable pairs. The API reports those as `null`, which is deliberately kept distinct from a genuine zero. + +### Geocode + +Turn addresses into coordinates, free-form or structured: + +```python +results = client.geocode.search(justrouting.GeocodeRequest( + text="Marina Bay Sands, Singapore", + limit=3, + filters=["countrycode:sg"], # one filter per entry +)) + +r = results.results[0] +print(r.formatted) # "Marina Bay Sands, 10 Bayfront Avenue, 018956, Singapore" +print(r.location()) # a Point: [103.859, 1.2834] +print(r.rank.confidence) +``` + +`structured` (an address as typed fields) is mutually exclusive with `text`; `bias` steers results toward a location. An empty `results` list means nothing matched — it is a valid answer, not an error. + +### Map Matching + +Snap a noisy GPS trace onto the road network: + +```python +match = client.map_matching.get(justrouting.MapMatchingRequest( + coordinates=trace, # GPS points, in chronological order + timestamps=ts, # optional: UNIX seconds, one per point + radiuses=rs, # optional: max snap distance per point, metres +)) + +print(f"{match.confidence * 100:.0f}% confidence") # 0 to 1 +``` + +`Match` extends `Route`, so `distance`, `duration`, `geometry`, and `legs` are all available. `get_all` also returns `tracepoints` aligned with the input coordinates — `None` entries mark points that could not be matched. Use `gaps="ignore"`, `tidy=True`, `waypoints=[0, 5]`, or `snapping="any"` to shape the matching. + +### Trip + +The fastest order to visit a set of coordinates: + +```python +resp = client.trip.get_all(justrouting.TripRequest( + coordinates=[depot, stop_a, stop_b], + roundtrip=False, # default (None) ends where it started +)) + +trip = resp.trips[0] +print([wp.name for wp in resp.waypoints]) # in visiting order +``` + +`source="first"` / `destination="last"` pin where the trip starts and ends. + +### Nearest + +The road segment closest to a coordinate: + +```python +wp = client.nearest.get(justrouting.NearestRequest( + coordinate=[103.8198, 1.3521], + number=3, # 2nd- and 3rd-nearest segments too +)) + +print(wp.name, wp.location, wp.distance) # street, snapped point, metres +print(wp.nodes) # OSM node IDs, when present +``` + +### Optimization + +Assign tasks to a fleet and order each vehicle's stops. + +```python +solution = client.optimization.solve(justrouting.OptimizationRequest( + vehicles=[justrouting.Vehicle( + id=1, start=depot, end=depot, capacity=[4], + )], + jobs=[ + justrouting.Job(id=1, location=stop_a, delivery=[1], service=300), + justrouting.Job(id=2, location=stop_b, delivery=[2], service=300), + ], +)) + +for route in solution.routes: + print(f"vehicle {route.vehicle}: {len(route.steps)} stops") +print(len(solution.unassigned), "task(s) could not be served") +``` + +Use `Shipments` instead of `Jobs` for pickup-and-delivery pairs that must be served in order by the same vehicle. + +### Health + +The only call that works without an API key, which makes it a useful connectivity check. + +```python +health = client.health.get() +print(health.ok(), health.upstreams) +``` + +## Error handling + +Every failure raises a subclass of `justrouting.Error`. Classify it with `except` clauses rather than matching on message text: + +```python +try: + route = client.routes.get(req) +except justrouting.QuotaExceededError: + # daily allowance used up — retrying will not help +except justrouting.RateLimitedError: + # throttled; the client already retried +except justrouting.CrossCountryError: + # coordinates span more than one country +except justrouting.NoRouteError: + # no road connects these points +``` + +| Exception | Meaning | +| --- | --- | +| `UnauthorizedError` | API key missing, invalid, or revoked | +| `RateLimitedError` | Throttled (per-second limit or daily quota) | +| `QuotaExceededError` | Daily quota exhausted; retrying will not help (subclasses `RateLimitedError`) | +| `PlanLimitExceededError` | Too many matrix coordinates, jobs, or vehicles | +| `CrossCountryError` | Coordinates span more than one country | +| `InvalidCoordinatesError` | Coordinate malformed or out of range | +| `NoRouteError` | No route exists between the points | +| `UpstreamUnavailableError` | Routing engine unreachable; usually transient | +| `InvalidRequestError` | Rejected locally before any request was sent | + +When the engine finds nothing — an empty `matchings`, `trips`, or `waypoints` list on an "Ok" response — map matching, trip, and nearest raise `Error` with `status_code == 200` and `osrm_code` `"NoMatch"`, `"NoTrip"`, or `"NoSegment"` (no dedicated subclass; check `e.osrm_code`). + +Catch `justrouting.Error` itself when you need the status code, engine code, or raw body: + +```python +except justrouting.Error as e: + print(f"HTTP {e.status_code}: {e.message}") + print(e.body) # raw response body, truncated +``` + +Failures that never produced an API response raise `TransportError` (network-level, retried like a 5xx) or `DecodeError` (unparseable success response) instead; neither is a subclass of `Error`. + +## Configuration + +| Argument | Default | Purpose | +| --- | --- | --- | +| `api_key` | — | Sent as `Authorization: Bearer` on every authenticated request | +| `base_url` | `https://api.justrouting.tech` | Target a local or staging server | +| `user_agent` | `justrouting-py/` | Identify your application | +| `max_retries` | `2` | Retry budget on top of the initial attempt | +| `backoff` | 500ms → 8s, jittered | Callable replacing the retry delay schedule | +| `timeout` | `30` | Seconds per HTTP attempt | + +Invalid options raise `ValueError` immediately. (The Go client defers them to the first request because its constructor cannot fail; Python constructors can.) + +### Timeouts and retries + +Rate limits (429), server errors (5xx), and transport failures are retried with exponential backoff and jitter; a `Retry-After` header takes precedence when present. Other 4xx responses are returned immediately — they would fail identically on a retry and would still consume quota. + +`Client(timeout=...)` covers a single HTTP attempt, like `http.Client.Timeout` in Go. Pass the per-call `timeout` argument to bound the whole retry sequence: + +```python +route = client.routes.get(req, timeout=30) # 30s total, retries included +``` + +Proxy configuration follows the standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables. + +## Things to know + +### Coordinates are `[longitude, latitude]` + +This is the GeoJSON order, and the reverse of the "lat, lng" used by most map UIs. Swapped coordinates are usually caught locally — a longitude in the latitude slot fails the `[-90, 90]` check before a request is sent — but a swap that stays in range will silently route somewhere unexpected. + +`Point` is a `list` subclass, so both of these work: + +```python +origin=[103.8198, 1.3521] +origin=justrouting.Point([103.8198, 1.3521]) +``` + +### Coordinates must share a country + +Every coordinate in a single request must fall within one country; the API routes each request to a per-country engine. A Singapore → Kuala Lumpur request fails with `CrossCountryError`. + +Supported countries: Brunei, Cambodia, Indonesia, Laos, Malaysia, Myanmar, the Philippines, Singapore, Thailand, and Vietnam. + +### Plan limits + +| | Free | Hobby | +| --- | --- | --- | +| Requests per day | 100 | 10,000 | +| Requests per second | 5 | 10 | +| Matrix coordinates | 100 | 500 | +| Jobs per optimization | 100 | 1,000 | +| Vehicles per optimization | 10 | 50 | + +Exceeding a size limit raises `PlanLimitExceededError`; exhausting the daily allowance raises `QuotaExceededError`. + +## Examples + +Runnable scripts live in [`examples/`](https://github.com/justrouting/py-client/tree/main/examples): + +```shell +export JUSTROUTING_API_KEY= +python examples/route.py +python examples/matrix.py +python examples/geocode.py +python examples/map_matching.py +python examples/trip.py +python examples/nearest.py +python examples/optimization.py +``` + +## Development + +```shell +pip install -e '.[dev]' +pytest +``` + +The default suite runs entirely against local `http.server` instances — no network access and no API key. + +Integration tests hit a live API and are behind a marker, so they never run by accident: + +```shell +pytest -m integration # health and auth only +JUSTROUTING_API_KEY= pytest -m integration +JUSTROUTING_BASE_URL=http://localhost:8080 pytest -m integration +``` + +## Differences from the Go client + +* Exceptions replace sentinel errors and `errors.Is`; `QuotaExceededError` subclasses `RateLimitedError`. +* Constructor options are keyword arguments; invalid ones raise `ValueError` immediately instead of being deferred to the first request. +* There is no `context.Context`; the per-call `timeout` argument plays the role of a context deadline. +* Matrix accessors return `float | None` instead of `(float, ok)`. +* There is no custom-HTTP-client option; use `base_url`, `timeout`, and the standard proxy environment variables instead. + +## License + +[MIT](https://github.com/justrouting/py-client/blob/main/LICENSE) + +## Related + +- [SDK Overview](/sdk/overview) +- [Go Client](/sdk/go-client) +- [JavaScript Client](/sdk/js-client)