A Django REST API that plans the cheapest way to fuel up on a road trip.
Give it a start and finish location anywhere in the USA, and it returns:
- the driving route (as geometry, ready to draw on a map)
- the cost-optimal set of fuel stops along the way, respecting a 500-mile vehicle range
- the total distance, gallons, and money spent on fuel (assuming 10 mpg)
A minimal HTML front end (with a live Leaflet map) is included too, so you can try it in a browser instead of Postman.
POST /api/route/ { "start": "...", "finish": "..." }
│
├─ 1. Geocode start + finish → Nominatim (2 calls)
├─ 2. Get the driving route + geometry → OSRM (1 call)
├─ 3. Find candidate fuel stations near the route → local DB query
└─ 4. Pick the cheapest combination of stops that never exceeds
the vehicle's range → dynamic programming
Only 3 external API calls per request, regardless of route length or how many fuel stations exist along it. That's possible because of two design decisions:
- Fuel stations are geocoded once, offline, not per-request. The
geocode_stationsmanagement command runs against the ~8,000-row OPIS price sheet ahead of time and writes lat/lon into the database. A live request never geocodes a station — it only looks up stations already sitting in the DB within a bounding box around the route. - The optimizer is a shortest-path problem, not a greedy "nearest cheap station" heuristic. Fuel stops are modeled as nodes on a line (distance-along-route), and a dynamic program finds the minimum-cost way to connect start → finish where every hop is ≤ 500 miles. This correctly handles cases where skipping a nearby expensive station in favor of a farther cheap one is the better move — a naive "always fill up at the cheapest station in range" approach can get this wrong.
| Piece | Choice | Why |
|---|---|---|
| Framework | Django 6.0.7 + Django REST Framework | latest stable Django |
| Routing | OSRM public demo server | free, no API key, one call returns full route geometry + distance |
| Geocoding | Nominatim (OpenStreetMap) | free, no API key |
| Database | SQLite | zero setup; swap the DATABASES config for Postgres in production |
| Fuel price data | Provided OPIS CSV, pre-geocoded offline | avoids per-station API calls at request time |
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
python manage.py migrate
# Load the pre-geocoded fuel price CSV into the database
python manage.py load_fuel_stations
python manage.py runserverThe API is now live at http://127.0.0.1:8000/.
Re-geocoding the price sheet yourself: if you want to regenerate
fuel/data/fuel_prices_geocoded.csvfrom the rawfuel/data/fuel_prices.csv, runpython manage.py geocode_stations. This is a one-time, offline data-prep step — it is not called during normal API requests. Before running it, openconfig/settings.pyand setNOMINATIM_USER_AGENTto your own name/contact info; Nominatim's usage policy rejects generic placeholder user agents with a 403.
Endpoint: POST /api/route/
Content-Type: application/json
Headers: Content-Type: application/json
Request body:
{
"start": "Dallas, TX",
"finish": "Chicago, IL"
}Response:
{
"start": {
"query": "Dallas, TX",
"latitude": 32.78,
"longitude": -96.80
},
"finish": {
"query": "Chicago, IL",
"latitude": 41.88,
"longitude": -87.63
},
"total_distance_miles": 925.4,
"vehicle_max_range_miles": 500,
"vehicle_mpg": 10,
"total_gallons": 92.54,
"total_fuel_cost": 267.15,
"fuel_stops": [
{
"name": "PILOT TRAVEL CENTER #123",
"address": "...",
"city": "...",
"state": "OK",
"latitude": 35.47,
"longitude": -97.52,
"price_per_gallon": 2.89,
"distance_along_route_miles": 210.3,
"gallons_purchased": 45.2,
"cost": 130.63
}
],
"route_geometry": [
[32.78, -96.80],
[32.79, -96.81]
]
}A ready-to-import Postman collection and environment are included in postman/.
| Status | Meaning |
|---|---|
400 |
Start/finish could not be geocoded, or the request body is invalid |
422 |
The route is technically valid but no combination of known fuel stations can bridge a gap within the 500-mile range (e.g. a remote stretch with no stations in the dataset) |
502 |
The routing service (OSRM) could not be reached |
Visit http://127.0.0.1:8000/ for a simple form-based UI that renders the route and fuel stops on a Leaflet map — the same optimize_route logic backs both the HTML view and the JSON API.
fuel-route-api/
├── config/ # Django project settings, root URLs
├── fuel/
│ ├── models.py # FuelStation model
│ ├── views.py # RoutePlanView (API) + home (HTML)
│ ├── serializers.py # DRF request/response schemas
│ ├── services/
│ │ ├── geocoding.py # Nominatim wrapper (per-request)
│ │ ├── routing.py # OSRM wrapper
│ │ ├── geo_utils.py # haversine distance, route sampling, bounding box
│ │ ├── optimizer.py # candidate station lookup + DP cost optimizer
│ │ └── route_optimizer.py # orchestrates the full pipeline
│ ├── management/commands/
│ │ ├── geocode_stations.py # one-time offline geocoding of the CSV
│ │ └── load_fuel_stations.py # loads the geocoded CSV into the DB
│ ├── data/ # raw + geocoded fuel price CSVs
│ ├── templates/, static/ # HTML front end
│ └── tests.py
└── postman/ # importable Postman collection + environment
python manage.py testTests cover the DP optimizer (including cases where a cheaper-but-farther station beats a nearer-but-pricier one, and cases requiring multiple stops) and the geocoding fallback logic. They run against mocked data with no network calls, so they're fast and deterministic.
- Vehicle range (500 mi) and fuel economy (10 mpg) are fixed per the assignment spec (
VEHICLE_MAX_RANGE_MILES/VEHICLE_MPGinsettings.py); the vehicle always starts with a full tank. - Fuel stations are only considered if they're within
FUEL_STATION_MAX_DETOUR_MILES(default: 5 miles) of the route line, and the optimizer's positions are based on distance-along-route rather than exact routed detour distance. - ~6% of stations in the provided price sheet couldn't be matched to a precise street address by Nominatim and fall back to their city center (
geocode_precision = "city_fallback"on theFuelStationmodel). - The OSRM public demo server and Nominatim's public instance are rate-limited and intended for light/demo use. For production traffic, point
OSRM_BASE_URLat a self-hosted OSRM instance (or a provider like OpenRouteService/Mapbox) and add caching/backoff for Nominatim. DEBUG = Trueand a checked-inSECRET_KEYare fine for this assessment but should never ship to production as-is.