Build a routing service: a practical guide to road-network routing

A routing service converts geographic waypoints into paths through a transportation network and returns distance, duration, geometry, and often turn-by-turn instructions. Building one requires more than drawing a line between coordinates: you must acquire and license road data, select a routing engine, preprocess a graph, expose an API, validate coordinate order and snapping behavior, and plan for map updates, traffic, observability, and capacity. This guide uses Project OSRM as a concrete, self-hostable implementation while distinguishing routing computation from geocoding, map rendering, and broader geographic analysis.

The central design decision is whether you need to operate the routing engine yourself or consume a hosted directions API. Self-hosting gives you control over data versions, profiles, request limits, and network behavior, but it also makes you responsible for preprocessing, storage, upgrades, monitoring, and data licensing. A practical starting point is Project OSRM with an OpenStreetMap extract. OSRM's documented pipeline uses a Lua profile during extraction, then prepares the resulting graph with either Multi-Level Dijkstra (MLD) or Contraction Hierarchies (CH). Its HTTP API supports route, nearest, table, match, trip, and tile services. (github.com)

How EDK fits

For: Software engineers, GIS developers, platform teams, and technical analysts building applications that need driving, walking, cycling, or other network-based travel calculations.

Routing is a direct example of computing about the physical world. A route is not merely a visual line: it is a constrained computation over roads, paths, turn restrictions, access rules, travel speeds, and transportation modes. The routing engine supplies one specialized computation layer for a transportation network, while a broader world model can provide the surrounding geographic context: roads and service roads, footways and cycleways, buildings, schools, parks, waterways, charging stations, hydrants, construction activity, or public-service locations. That distinction matters. A routing service answers questions such as “what path connects these points under this profile?” A world-model interface can additionally help find the relevant physical features, relate them to the route, filter nearby assets, compare regions, or compute what lies within a distance or along a corridor. E2's connection is therefore strongest when routing is combined with discovery and spatial computation—not when a routing engine is presented as a replacement for every GIS component.

  • •Route-aware asset analysis: compute a route between facilities, then find nearby roads, service locations, charging stations, schools, parks, or other physical features around the resulting corridor.
  • •Operational territory planning: compare travel times from depots to customers or public assets, aggregate destinations by reachable area, and rank locations using both network travel and geographic context.
  • •Change and constraint analysis: combine route results with current geographic features such as construction activity, road access, waterways, or pedestrian infrastructure to identify where a planned route may require additional review. The routing engine itself does not establish that those features exist or that they are current.
Concrete engine used in this guide
Project OSRM
Primary input example
OpenStreetMap PBF extract
Preparation model
Lua profile followed by MLD partition and customization, or CH contraction
Core request shape
Semicolon-separated longitude,latitude waypoints
Separate concerns
Geocoding, rendering, live traffic, and asset analysis are not automatically provided by the routing engine

The EDK outcome

Before you start

Basic command-line and Docker familiarity.

A machine with enough disk, memory, and CPU for the geographic extract and preprocessing job; exact requirements depend on region size, engine version, profile, and hardware.

A licensed road-network data source in PBF or another format supported by the selected engine.

A clear definition of supported travel modes, geographic coverage, response format, waypoint limits, and freshness expectations.

A test set of known routes and edge cases for every supported profile.

Learning objectives

Explain the difference between road-network data, a prepared routing graph, and an application-facing routing API.

Prepare a small OSRM graph from an OpenStreetMap PBF extract using Docker.

Issue and validate a route request with explicit coordinate order and response options.

Design operational boundaries for profiles, updates, rate limits, testing, and geographic context.

Relate route geometries to broader physical-world analysis without confusing routing with geocoding or general GIS computation.

Verification

A direct route request returns a successful response for a known pair of points.

The service rejects reversed or out-of-range coordinates instead of silently producing an unexpected route.

Returned geometry endpoints are close to the requested waypoints.

Known one-way, access-restricted, and disconnected-network tests produce the expected behavior.

The deployed graph, profile, and data-extract versions are recorded and reproducible.

A route can be stored and queried against nearby geographic features using explicit distance units.

Troubleshooting

No route found: verify coordinate order, profile compatibility, network connectivity, extract coverage, and whether the points are too far from routable edges.

The container starts but requests fail: confirm that all prepared `.osrm.*` files are mounted, that the algorithm matches preprocessing, and that the graph base path is correct.

Unexpected route choice: inspect the selected profile, road access tags, turn restrictions, ferry behavior, data age, and any custom speed or penalty updates.

Profile changes have no effect: rebuild extraction and graph preparation; OSRM profiles are applied during preprocessing rather than selected arbitrarily at request time. (github.com)

Slow matrix requests: cap the number of sources and destinations, separate matrix traffic from interactive route traffic, and evaluate whether CH is more appropriate for the workload.

Nearby-feature results look wrong: check geometry validity, SRIDs, geography versus geometry casts, distance units, and whether the query is measuring to the route line or only to its endpoints.

Next steps

Add a geocoder or place-resolution component so users can submit addresses and place names rather than only coordinates.

Add a route-matching workflow for GPS traces when the application needs to infer which roads were traveled.

Introduce authoritative closure, restriction, or traffic feeds only after defining how they map to the prepared graph and how conflicts are resolved.

Build route-corridor analysis that combines network paths with nearby roads, buildings, facilities, environmental features, or public-service records.

Compare OSRM and GraphHopper on the same fixed test suite, region, profiles, and workload before making an engine decision.

Prerequisites and architecture

Before writing an endpoint, define the service contract. Decide which travel modes you support, whether coordinates or place identifiers are accepted, whether alternatives and turn-by-turn steps are required, and whether responses must include GeoJSON, encoded polylines, or only distance and duration. Also define what “current” means. A route computed from a static OpenStreetMap extract is not the same as a route using live traffic, temporary closures, or agency-maintained restrictions.

For a self-hosted implementation, the main components are an input-data pipeline, a routing engine, an HTTP service, and an application-facing adapter. OpenStreetMap is a common source for road-network data, but it is community-maintained and subject to the Open Database License. The OpenStreetMap Foundation requires attribution and makes clear that derived databases can carry additional licensing obligations. Review the license for your exact distribution model before shipping a service or redistributing processed data. (openstreetmap.org)

  • •Runtime: Docker or a native OSRM installation.
  • •Network data: an extract in PBF format from a source such as Geofabrik or another licensed distributor.
  • •Preparation: an OSRM Lua profile for car, bicycle, foot, or another supported mode.
  • •API layer: a web service that validates and limits requests before calling OSRM.
  • •Testing data: fixed origin-destination pairs covering motorways, one-way streets, turn restrictions, disconnected roads, ferries, bridges, and pedestrian paths.

Step 1: download and preprocess the road network

The following example uses a small Berlin extract to keep the first build manageable. Replace the filename with an extract whose license and geographic coverage fit your application. Project OSRM's documented Docker workflow downloads an OSM PBF file, runs `osrm-extract` with a profile, partitions the graph, customizes it, and starts `osrm-routed`. (github.com)

```bash mkdir routing-data cd routing-data wget https://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf export OSRM_IMAGE=ghcr.io/project-osrm/osrm-backend export PBF=berlin-latest.osm.pbf # Convert OSM data into the OSRM graph using the vehicle profile. docker run --rm -t -v "$PWD:/data" \ "$OSRM_IMAGE" \ osrm-extract -p /opt/car.lua "/data/$PBF" # Prepare the graph for Multi-Level Dijkstra routing. docker run --rm -t -v "$PWD:/data" \ "$OSRM_IMAGE" osrm-partition "/data/$PBF.osrm" docker run --rm -t -v "$PWD:/data" \ "$OSRM_IMAGE" osrm-customize "/data/$PBF.osrm" ```

  • •Pin the container image by a tested release tag rather than relying on an unqualified moving tag.
  • •Record the extract timestamp, download source, profile revision, engine version, and preprocessing command.
  • •Use separate output basenames when preparing the same PBF for car, bicycle, and foot profiles.
  • •Expect preprocessing time and memory requirements to grow substantially with geographic coverage.

Step 2: start the engine and expose a minimal route endpoint

Start the prepared graph with MLD and bind the container's port 5000 to a private host port. The OSRM HTTP API uses a path such as `/route/v1/driving/{coordinates}`. Coordinates are semicolon-separated and expressed as longitude,latitude pairs. The response can include route geometry, steps, annotations, and waypoints. (project-osrm.org)

```bash docker run --rm -it \ -p 5000:5000 \ -v "$PWD:/data" \ "$OSRM_IMAGE" \ osrm-routed --algorithm mld "/data/$PBF.osrm" ```

  • •Use `steps=true` only when turn-by-turn instructions are needed; detailed responses are larger.
  • •Use GeoJSON when the client already works with geographic features; use a compact encoded geometry when payload size matters.
  • •Cap waypoint counts and matrix dimensions to prevent expensive requests from exhausting the service.
  • •Keep the engine endpoint off the public internet unless it is protected by an API gateway or equivalent controls.

Step 3: model profiles, updates, and geographic context

A routing profile determines which network edges are usable and how they are weighted. In OSRM, profiles are Lua scripts applied during extraction, so changing the profile requires rebuilding the prepared data and reloading the service. This is an important operational constraint: profile changes are deployment events, not ordinary request parameters. (github.com)

Use profiles for stable differences such as car versus bicycle or foot access. Do not treat a profile as a substitute for a complete live-traffic system. OSRM's MLD pipeline can apply segment-speed and turn-penalty updates during customization, but you need a reliable mapping between external observations and the prepared graph's edges. The project documentation describes these inputs as CSV updates and notes that customization can be repeated without repartitioning. (github.com)

  • •Version profiles and graph artifacts together.
  • •Use a blue/green or shadow deployment when rebuilding a regional graph.
  • •Never infer live closures solely from an old static extract.
  • •For corridor analysis, preserve the route geometry and query nearby features with an explicitly chosen distance and coordinate system.

Step 4: validate, operate, and scale the service

Routing errors are often plausible rather than obvious. A request may return a route that is technically valid but operationally wrong because a coordinate snapped to the wrong carriageway, a private road was treated as accessible, a ferry was selected, or the underlying map was stale. Build a regression suite with known expected properties rather than comparing every vertex exactly.

At minimum, test that routes stay within the intended region, do not cross prohibited barriers, honor one-way behavior, produce non-negative distance and duration, and return a geometry whose endpoints are near the requested points. Test each supported profile independently. If your application depends on emergency access, truck restrictions, loading zones, seasonal roads, or private facilities, obtain authoritative supplemental data and document how it is merged; the base routing engine cannot establish those facts automatically.

  • •Load-test realistic coordinate distributions, not only short routes in a city center.
  • •Use timeouts and cancellation when upstream clients disconnect.
  • •Publish a health check that confirms the graph is loaded, not merely that the process is alive.
  • •Retain enough metadata to reproduce a disputed route.

Workflow

01

1. Define the service contract

Specify modes, waypoint limits, geometry formats, steps, alternatives, matrix behavior, freshness, error semantics, and licensing responsibilities before choosing an engine. Decide which concerns belong outside routing: address search, map display, traffic, closures, and nearby-feature analysis.

Example request contract: POST /v1/routes with {"profile":"driving","coordinates":[[13.388860,52.517037],[13.385983,52.496891]],"steps":true,"geometry":"geojson"}
02

2. Download an appropriately licensed extract

Obtain a regional PBF extract from a suitable distributor, record its timestamp and source, and preserve the attribution and license information required for your use. Start with a small region while developing.

wget https://download.geofabrik.de/europe/germany/berlin-latest.osm.pbf
03

3. Build the prepared routing graph

Run the selected OSRM profile during extraction, then prepare the graph with MLD partitioning and customization. Treat the resulting group of files as one versioned deployment artifact.

docker run --rm -t -v "$PWD:/data" ghcr.io/project-osrm/osrm-backend osrm-extract -p /opt/car.lua /data/berlin-latest.osm.pbf
docker run --rm -t -v "$PWD:/data" ghcr.io/project-osrm/osrm-backend osrm-partition /data/berlin-latest.osrm
docker run --rm -t -v "$PWD:/data" ghcr.io/project-osrm/osrm-backend osrm-customize /data/berlin-latest.osrm
04

4. Start and query the engine

Run osrm-routed with the matching algorithm, then issue a small direct request. Confirm that coordinates, profile, response geometry, distance, duration, and steps match expectations before adding an application adapter.

docker run --rm -it -p 5000:5000 -v "$PWD:/data" ghcr.io/project-osrm/osrm-backend osrm-routed --algorithm mld /data/berlin-latest.osrm
curl 'http://127.0.0.1:5000/route/v1/driving/13.388860,52.517037;13.385983,52.496891?steps=true&geometries=geojson&overview=full'
05

5. Add validation and a stable API layer

Validate numeric ranges, coordinate order, waypoint counts, profile names, request body size, and timeouts. Normalize upstream errors, protect the engine with authentication and rate limits, and include graph/profile versions in logs and responses.

Reject: latitude outside [-90,90], longitude outside [-180,180], empty coordinates, unsupported profiles, excessive waypoints, and requests that exceed the configured timeout.
06

6. Test route behavior and surrounding analysis

Build regression cases for one-way roads, disconnected networks, bridges, ferries, private roads, pedestrian paths, and profile-specific access. When routes must be related to physical assets, store their geometries and run explicit spatial queries in a GIS or spatial database.

SELECT id FROM assets WHERE ST_DWithin(geom::geography, ST_GeomFromGeoJSON(:route)::geography, 100);
07

7. Deploy with versioning and observability

Version the extract, profile, engine image, preprocessing commands, and configuration. Use health checks that confirm the graph is loaded, monitor latency and errors, separate interactive routes from large matrix jobs, and use blue/green replacement when refreshing graph artifacts.

Record per request: request_id, profile, graph_version, engine_version, waypoint_count, duration_ms, response_code, and route_error_code.

Questions teams ask

Can I build a routing service directly from GeoJSON road lines?

Not reliably without substantial additional work. A routing engine needs a connected graph with nodes, directed edges, access rules, turn relationships, speeds or costs, and often restrictions. Raw line features may be useful source geometry, but they do not automatically encode a routable network. Use a routing engine's supported preprocessing pipeline or build and validate an equivalent graph explicitly.

Should I use OSRM, GraphHopper, or a hosted directions API?

Use OSRM when you want a focused, self-hosted engine and are comfortable preparing graph artifacts. Consider GraphHopper when profile and custom-model controls are central to the application; its documentation describes customizable profiles and routing behavior. Use a hosted API when operational simplicity, global coverage, managed updates, or vendor-provided traffic matter more than infrastructure control. Compare licensing, geographic coverage, commercial terms, latency, request limits, and the exact features you need rather than choosing by benchmark claims alone. (github.com)

Why does my route fail even though both points are on a map?

The points may be outside the prepared network, too far from a routable edge, located on disconnected components, assigned to an incompatible profile, or affected by access restrictions. Coordinate-order mistakes are also common. Verify longitude,latitude order, test the engine's nearest behavior, inspect the selected extract, and compare the failing points with the actual routable network.

How do I find physical features near a computed route?

Store the returned route geometry in a spatially enabled database and use a corridor or proximity query. With PostGIS, `ST_DWithin` can test whether features fall within a specified distance, while `ST_LineLocatePoint` can provide a relative position along a line for ordering or linear referencing. Make the coordinate system and distance units explicit. (postgis.net)

Keep exploring

Sources

More in Guides