Home | Send Feedback | Share on Bluesky |

GraphHopper, routing engine for OpenStreetMap data

Published: 22. April 2026  •  angular

At its core, GraphHopper is an open-source routing engine designed to process geographic data into navigable paths. Written in Java and powered by OpenStreetMap (OSM) data, it provides the underlying logic needed to calculate directions, distances, and travel times across different modes of transport.

GraphHopper can calculate routes for cars, bicycles, pedestrians, and custom vehicle profiles. It also supports features like:

GraphHopper allows you to customize routing behavior with "custom models" that adjust how the engine prioritizes certain road types, speeds, or access rules. For example, you can create a custom model that avoids toll roads for cars or prefers bike lanes for cycling routes. Depending on the features you need you can self-host the engine or use the hosted GraphHopper Directions API, which covers more than just routing and includes geocoding, matrices, and route optimization as managed services.

Common use cases for a routing engine like GraphHopper include:

Self-hosted vs Hosted GraphHopper

GraphHopper is written in Java so you can embed it as a library in your own JVM application. A more common approach is to run it as a standalone server that exposes a REST API. Running GraphHopper locally is perfectly fine as long as you have the necessary resources, especially when you use a large OSM extract. Servicing a small country like Switzerland is not a big problem, but serving the whole of Europe or the planet requires more powerful hardware and careful tuning.

GraphHopper offers a hosted API at GraphHopper Directions API. The self-hosted version while free does not provide the same functionality as the hosted API. The feature matrix shows you what is included in the self-hosted version and what is only available in the hosted API.

You can fill in these gaps with custom code and manual data feeds, but that requires more work and maintenance on your side. The hosted API is a good choice if you want to get started quickly, need features like real-time traffic or geocoding, or prefer not to manage the infrastructure yourself. The service offers a free tier, which is great for testing and small projects, and paid plans for higher usage levels. Check out the GraphHopper pricing page for details on the cost of the hosted service.

Here are a few of the services that are missing in the self-hosted version and can be implemented with custom code and libraries:


The demo application in this repo only uses the Routing API, which is fully supported in the self-hosted version.

Downloading OSM data

Before we can run GraphHopper locally, we need to get some OpenStreetMap data in .osm.pbf format. The easiest way to get regional extracts is from Geofabrik. For this demo, we will use the Switzerland extract, which is a good size for local testing and development.

Installation

You could simply download the jar file from the GitHub project page and run it with Java (java -jar), but for a more robust and reproducible setup, this demo uses Docker to containerize the GraphHopper server. That way you can run it on any machine with Docker installed without worrying about Java versions, dependencies, or environment configuration.

The demo uses the following Dockerfile to build the image. It starts from the Eclipse Temurin JRE base image, then downloads the GraphHopper web JAR for the specified version and adds it to the container. Adjust the GRAPHHOPPER_VERSION argument to the latest release or the version you want to use.

FROM eclipse-temurin:26-jre

ARG GRAPHHOPPER_VERSION=11.0

WORKDIR /opt/graphhopper

ADD https://github.com/graphhopper/graphhopper/releases/download/${GRAPHHOPPER_VERSION}/graphhopper-web-${GRAPHHOPPER_VERSION}.jar /opt/graphhopper/graphhopper-web.jar

Dockerfile.graphhopper

Then this docker compose file runs the container. It builds the image from the Dockerfile, sets the working directory, and defines the command to start the GraphHopper server with the appropriate JVM options and system properties. The ports are exposed for both the application and admin interfaces, and a volume is mounted to persist the graph cache on the host. You can set the OSM_PBF environment variable to specify a different OpenStreetMap extract if you want to use something other than switzerland-latest.osm.pbf.

services:
  graphhopper:
    build:
      context: .
      dockerfile: Dockerfile.graphhopper
      args:
        GRAPHHOPPER_VERSION: ${GRAPHHOPPER_VERSION:-11.0}
    container_name: graphhopper
    working_dir: /workspace
    command:
      - java
      - -Xms1g
      - -Xmx4g
      - -Ddw.graphhopper.datareader.file=/workspace/${OSM_PBF:-switzerland-latest.osm.pbf}
      - -Ddw.graphhopper.graph.location=/workspace/graph-cache
      - -jar
      - /opt/graphhopper/graphhopper-web.jar
      - server
      - /workspace/config.yml
    ports:
      - "8989:8989"
      - "8990:8990"
    volumes:
      - ./:/workspace

docker-compose.yml

Note that the first startup will take some time as GraphHopper imports the OSM data and builds the graph cache. Subsequent restarts will be much faster since the cache is persisted on the host. Hosting a larger extract or using more complex profiles may require more memory and CPU resources, so adjust the JVM options and Docker resource limits as needed.

Configuration

The config.yml file contains the main GraphHopper configuration. It is an important part of the setup because it defines how the graph is built, which profiles are available, and how the server behaves. Here are few of the key sections of the config and what they do:

graphhopper.datareader.file

This configures the path to the .osm.pbf file that GraphHopper will import. It is intentionally empty here because Docker passes the real path via the JVM system property -Ddw.graphhopper.datareader.file=....

  datareader.file: ""

config.yml


graphhopper.graph.location

This is where the imported graph and preprocessed routing data are stored. In this repo it maps to the host folder graph-cache/. Setting up the graph takes some time, but it only needs to happen once as long as the cache is preserved, like in this setup.

  graph.location: graph-cache

config.yml


graphhopper.profiles

These are the routing profiles your server will accept at request time. In this demo we enable car and foot. The custom model files contain a set of rules that adjust routing behavior for the specified profile. For example, car.json might include rules that avoid pedestrian paths and prefer faster roads, while foot.json might favor sidewalks and footpaths.

  profiles:
   - name: car
#     turn_costs:
#       vehicle_types: [motorcar, motor_vehicle]
#       u_turn_costs: 60
#     for more advanced turn costs, see #2957 or bike_tc.yml
     custom_model_files: [car.json]

#   You can use the following in-built profiles. After you start GraphHopper it will print which encoded values you'll have to add to graph.encoded_values in this config file.
#
   - name: foot
     custom_model_files: [foot.json, foot_elevation.json]

config.yml

The GraphHopper documentation has more details on how to write custom model files and what options are available.


graphhopper.profiles_ch

This enables Contraction Hierarchies for the configured profiles. CH is GraphHopper's fast prepared-routing mode. This requires more RAM/disk space but speeds up route calculations.

  profiles_ch:
    - profile: car
    - profile: foot

config.yml


graphhopper.graph.encoded_values

Encoded values tell GraphHopper which road attributes to preserve in the graph. They matter when you want custom models, path details, or route analysis features.

  graph.encoded_values: car_access, car_average_speed, foot_access, hike_rating, mtb_rating, foot_priority, foot_average_speed, average_slope, country, road_class, foot_road_access

config.yml

In this config, the most notable values are:


routing.snap_preventions_default

When GraphHopper snaps raw coordinates to the road network, this default avoids attaching the points to tunnels, bridges, or ferries unless explicitly requested otherwise.

  routing.snap_preventions_default: tunnel, bridge, ferry

config.yml


import.osm.ignored_highways

Depending on your use case, you might want to exclude certain road classes from the graph. This example ignores trunk roads (major highways). This setting can speed up the import and reduce the graph size, and also means that routes will not use those road types.

  import.osm.ignored_highways: trunk # typically useful for non-motorized routing

config.yml

For example if you are building a pedestrian routing app, you might want to ignore motorway, trunk, and primary highways to ensure that routes do not include those roads. For a cycling app, you might want to ignore motorway and trunk but keep primary roads. The exact classes to ignore depend on your use case and the typical road network in your area of interest.


graph.dataaccess.default_type

This keeps GraphHopper on the faster, in-memory-oriented default storage mode. It is a sensible choice when the machine has enough RAM. Another option is MMAP_STORE, which uses memory-mapped files and can reduce RAM usage at the cost of some performance. For a demo with a Switzerland extract, RAM_STORE is usually fine, but for larger extracts or smaller machines you might want to switch to MMAP_STORE.

  graph.dataaccess.default_type: RAM_STORE

config.yml


server

The server section configures the application and admin connectors. The application connector serves the routing API, while the admin connector serves health checks and metrics. Both are configured to listen on all interfaces in this setup.

server:
  application_connectors:
  - type: http
    port: 8989
    bind_host: 0.0.0.0
    # increase GET request limit - not necessary if /maps UI is not used or used without custom models
    max_request_header_size: 50k
  request_log:
      appenders: []
  admin_connectors:
  - type: http
    port: 8990
    bind_host: 0.0.0.0

config.yml

Routing API basics

This blog post focuses on just one of the provided endpoints: the Routing API

After starting GraphHopper you can access the Routing API with a call to http://localhost:8989/route. The endpoint supports both GET and POST requests. When sending a POST request you include the parameters as JSON in the request body.

In this demo we will use GET requests. A simple GET request can look like this

curl http://localhost:8989/route?point=47.3769,8.5417&point=47.3717,8.5423&profile=foot&instructions=false&points_encoded=false

The point parameter (can be repeated) defines the coordinates of the route. You need at least two points, one for the origin and one for the destination. You can also include via points if you want to specify intermediate stops along the route.

profile specifies which routing profile to use. The profile determines the routing rules and preferences applied during route calculation. The available profiles depend on how you configured your GraphHopper server and which custom model files you included. In the self-hosted GraphHopper server used in this blog post, I only enabled the car and foot profiles.

With the instructions parameter, you can specify if turn-by-turn instructions should be included in the response.

points_encoded allows changing the encoding of location data in the response. The default is polyline encoding, which is compact but requires special client code to unpack. If false the response returns coordinate pairs (lat, lon) in a GeoJSON-like format.


GraphHopper returns the response as JSON. The JSON typically includes top-level metadata plus one or more route candidates in paths.

The most useful fields are:

Inside each item in paths, common fields include:

An abridged response looks like this:

{
  "hints": {
    "visited_nodes.sum": 58,
    "visited_nodes.average": 58
  },
  "info": {
    "copyrights": ["GraphHopper", "OpenStreetMap contributors"],
    "took": 2
  },
  "paths": [
    {
      "distance": 1791.011,
      "weight": 307.852443,
      "time": 370962,
      "bbox": [11.539424, 48.118343, 11.558901, 48.122364],
      "points_encoded": false,
      "points": {
        "type": "LineString",
        "coordinates": [
          [11.539424, 48.118352],
          [11.540387, 48.118368],
          [11.558901, 48.122364]
        ]
      },
      "instructions": [],
      "details": {},
      "ascend": 6.33,
      "descend": 25.06,
      "snapped_waypoints": {
        "type": "LineString",
        "coordinates": [
          [11.539424, 48.118352],
          [11.558901, 48.122364]
        ]
      }
    }
  ]
}

For more details about the Routing API, check out the GraphHopper routing documentation and the API explorer.

Frontend demo

The Angular app in this repo demonstrates a simple integration with the GraphHopper Routing API. It allows users to click on a map to set start and end points, then it calls the /route endpoint to calculate and display the route. The app uses MapLibre GL JS for map rendering and interaction, and it uses the vector tiles from Swisstopo.

Request

After the user sets the start and end points, the app builds a GET request to /route with the appropriate query parameters. RouteProfile is either car or foot. The demo uses the unencoded points response format for easier integration.

  getRoute(profile: RouteProfile, fromLat: number, fromLng: number, toLat: number, toLng: number) {
    const params = new HttpParams()
      .append('point', `${fromLat},${fromLng}`)
      .append('point', `${toLat},${toLng}`)
      .set('profile', profile)
      .set('instructions', false)
      .set('points_encoded', false);

    return this.http.get<GraphhopperRouteResponse>(this.routeUrl, { params }).pipe(
      map((response) => {
        const [path] = response.paths ?? [];

        if (!path) {
          throw new Error('GraphHopper returned no route');
        }

        return {
          distanceMeters: path.distance,
          timeMillis: path.time,
          geometry: path.points,
        } satisfies RouteSummary;
      }),
    );
  }

route-api.ts


Response

The TypeScript code uses these interfaces to model the GraphHopper response. LineString is a GeoJSON type that represents the route geometry as an array of coordinate pairs.

interface GraphhopperPath {
  distance: number;
  time: number;
  points: LineString;
}

interface GraphhopperRouteResponse {
  paths: GraphhopperPath[];
}

route-api.ts


Draw route

After GraphHopper returns the route, the app extracts the geometry and draws it on the map. It creates a GeoJSON feature collection from the LineString geometry and updates the map source. Then it fits the map view to the route bounds with some padding.

  private drawRoute(geometry: LineString): void {
    if (!this.map) {
      return;
    }

    const coordinates = geometry.coordinates as [number, number][];
    const featureCollection: FeatureCollection<LineString> = {
      type: 'FeatureCollection',
      features: [
        {
          type: 'Feature',
          geometry,
          properties: {},
        },
      ],
    };

    const updateSource = () => {
      if (!this.map || coordinates.length === 0) {
        return;
      }

      const source = this.map.getSource('route') as GeoJSONSource | undefined;
      if (!source) {
        return;
      }

      source.setData(featureCollection);

      const bounds = coordinates.reduce(
        (currentBounds: maplibregl.LngLatBounds, coordinate: [number, number]) => {
          return currentBounds.extend(coordinate);
        },
        new maplibregl.LngLatBounds(coordinates[0], coordinates[0]),
      );

      this.map.fitBounds(bounds, {
        padding: 48,
        duration: 800,
      });
    };

    if (this.map.isStyleLoaded()) {
      updateSource();
      return;
    }

    this.map.once('load', updateSource);
  }

app.ts

Wrapping up

GraphHopper is a powerful and flexible routing engine that can be either self-hosted or accessed via a hosted API. The self-hosted version gives you full control over the data, profiles, and routing logic, but requires more setup and maintenance, and you have to provide your own hardware resources. Depending on the size of the OSM extract and the complexity of the profiles, hosting GraphHopper locally can be resource-intensive. The import process can take a significant amount of time, especially for larger extracts, and it may require several gigabytes of RAM and disk space.

The hosted GraphHopper Directions API is a convenient option if you want to get started quickly, need features like real-time traffic or geocoding, or prefer not to manage the infrastructure yourself. It offers a free tier for testing and small projects, and paid plans for higher usage levels. The hosted API also provides additional features that are not available in the self-hosted version, such as route optimization and matrix calculations, which can save you time and effort if you need those capabilities. And the big plus is that the hosted API covers the whole world, so you can easily serve routes for any location without worrying about data management or hardware requirements.