How to Collect and Reorder Route Waypoints in a Web App

API or service
Route Waypoint Selector + Address Autocomplete API
Task
Addresses and stop order → resolved route waypoints
Examples
JavaScript
Difficulty
Beginner
Time
8 min

Collect and reorder route waypoints in a web app

A user needs to enter an origin, a destination, and optional intermediate stops before your application can calculate a route. Each typed address must be resolved to latitude and longitude, and the application must preserve the user's chosen stop order.

Use @geoapify/route-waypoint-selector to provide this interface. The library uses Geoapify Address Autocomplete and lets users search for, add, remove, edit, and drag waypoints into order. It returns ordered waypoint objects containing a label, latitude, longitude, and an optional application ID.

The selector collects route inputs; it does not calculate or display the route. Your application decides when to pass the resolved coordinates to the Routing API or another routing service.

Task flow: typed addresses → autocomplete selections → ordered { label, lat, lon } waypoints → routing request.

Plan the waypoint collection experience

  • Require selected suggestions: Typed text alone is not a resolved waypoint. Enable route calculation only when at least two waypoints have a label, latitude, and longitude.
  • Keep stable IDs: Assign an application ID when waypoints correspond to deliveries, appointments, or other records. Use the ID—not the current array position—to keep markers and business data connected when stops move.
  • Define the stop order: The array order is the route order. Let users drag intermediate stops, but make it clear whether the first and last items represent a fixed origin and destination.
  • Choose when to calculate: A “Build route” button avoids a new routing request after every edit. Live recalculation can work when it is debounced and stale requests are cancelled or ignored.
  • Separate collection from optimization: Drag-and-drop records the order selected by the user. To calculate a better intermediate-stop order, send the resolved waypoints to the Routing API with optimize_stops=true.
  • Show incomplete state: Keep unresolved or cleared waypoints visibly different from selected addresses, and do not silently send their text as coordinates.
  • Support recovery: Preserve the current waypoint list when a routing request fails so the user can retry without entering the stops again.
  • Clean up the component: Call destroy() when the view is permanently removed so owned DOM listeners and autocomplete resources are released.

Add the route waypoint selector with JavaScript

Use @geoapify/route-waypoint-selector when the application needs a ready-made waypoint form with address autocomplete and drag-and-drop reordering. Install the package; its autocomplete dependency is installed automatically.

npm install @geoapify/route-waypoint-selector

Add a container for the selector and a separate action for calculating the route:

<div id="waypoint-selector"></div>
<button id="build-route" type="button" disabled>Build route</button>

Import matching styles for the waypoint selector and its autocomplete fields, then enable the button only when every waypoint is resolved:

import { WaypointSelector } from "@geoapify/route-waypoint-selector";
import "@geoapify/geocoder-autocomplete/styles/minimal.css";
import "@geoapify/route-waypoint-selector/styles/core.css";
import "@geoapify/route-waypoint-selector/styles/theme-minimal.css";

const apiKey = "YOUR_API_KEY";
const buildRouteButton = document.querySelector("#build-route");

const isResolved = ({ label, lat, lon }) =>
  Boolean(label) && Number.isFinite(lat) && Number.isFinite(lon);

const selector = new WaypointSelector("#waypoint-selector", apiKey, {
  onChange: (waypoints) => {
    // A route needs at least two fully resolved waypoints.
    buildRouteButton.disabled =
      waypoints.length < 2 || !waypoints.every(isResolved);
  }
});

buildRouteButton.addEventListener("click", async () => {
  const waypoints = selector.getWaypoints();

  // Routing API query coordinates use latitude,longitude order.
  const waypointValue = waypoints
    .map(({ lat, lon }) => `${lat},${lon}`)
    .join("|");

  const query = new URLSearchParams({
    waypoints: waypointValue,
    mode: "drive",
    format: "geojson",
    apiKey
  });

  const response = await fetch(
    `https://api.geoapify.com/v1/routing?${query}`
  );

  if (!response.ok) {
    throw new Error(`Routing failed with status ${response.status}`);
  }

  const route = await response.json();
  console.log(route);
});

// Call selector.destroy() when this view is permanently removed.

WaypointSelector returns waypoint objects as { label, lat, lon, id? }. The Routing API expects each query waypoint as latitude,longitude, separated by |; URLSearchParams safely encodes the complete value.

The package provides the input and autocomplete behavior. The Routing API produces the route geometry, distance, travel time, and directions. Rendering that route on a map remains the responsibility of your application and map library.

Handle waypoint state in production

  • Keep the full waypoint objects in application state instead of storing only their display labels.
  • Use onChange to update the canonical ordered list. Use the more specific onAdd, onRemove, onReorder, or onWaypointChange callbacks only when the application needs event-specific behavior.
  • Treat manual edits after address selection as unresolved until the user selects another autocomplete suggestion.
  • Key map markers and domain records by stable waypoint IDs so drag-and-drop reordering does not attach data to the wrong stop.
  • Prevent duplicate submissions while a route request is pending. Handle authorization errors, quota exhaustion, network failures, and empty route results without clearing the waypoint form.
  • Restrict browser API keys to the application's allowed origins. Never ship an unrestricted key in client code.
  • Keep a visible label for the selector, expose clear add/remove controls, preserve keyboard reordering, and announce changes for assistive technology.
  • Call selector.destroy() during the framework's unmount or destroy lifecycle.

If the user changes the order manually, send that order to the Routing API without optimize_stops. If the application should reorder only the intermediate stops, set optimize_stops=true; the origin and destination remain fixed.