Series 2: My Next.js 16 + OpenLayers + TypeScript Starter Kit — A Modern Map Apps Setup
Source: Dev.to
Why I Built This (Again)
A few weeks ago I released a Next.js + Leaflet starter kit as the first step in a mapping starter‑kit series. After wrapping that up, the next logical piece was OpenLayers—especially for developers working on more advanced GIS workflows.
Leaflet is great for lightweight, interactive maps, but many GIS‑driven projects need features like vector tiles, custom projections, high‑precision geometry rendering, advanced interactions, and performant handling of large GeoJSON datasets. That’s where OpenLayers really shines.
I rebuilt the entire starter from the ground up—same UI, same structure, same developer experience—but powered by OpenLayers to support real GIS use cases. This isn’t a replacement for the Leaflet version. Once the OpenLayers kit is out, I’ll complete the lineup with a Next.js + MapLibre starter kit for modern vector‑tile‑driven maps and 3D visualizations.
A production‑ready Next.js 16 starter with vanilla OpenLayers and the same Google‑Maps‑inspired UI from the Leaflet starter.
Technology Stack
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 16 | App Router, Server Components |
| React | 19 | UI Framework |
| OpenLayers | 10 | Mapping (vanilla, no wrapper) |
| TypeScript | 5 | Type safety |
| Tailwind CSS | 4 | Styling |
| shadcn/ui | Latest | Accessible UI components |
Features
- Base map setup – OpenLayers
MapandViewwith proper initialization - Multiple tile providers – OpenStreetMap, Satellite, Dark mode
- Theme‑aware tiles – Auto‑switches based on light/dark mode
- GeoJSON rendering – Custom styling and fit‑to‑bounds
- Country search – Debounced search with keyboard navigation
- Custom markers – Add markers anywhere with popups
- Context menu – Right‑click to copy coordinates, add markers, measure
- Measurement tools – Distance and area with interactive drawing
- POI management – Full CRUD with 14 categories,
localStoragepersistence - Geolocation – Find user location with accuracy circle
- Responsive layout – Mobile drawer, desktop sidebar
- Error boundaries – Graceful error handling
- Dark mode – Full theme support
Coordinate Conversion Utilities
OpenLayers uses [lng, lat] with Web Mercator projection, while most APIs use [lat, lng]. The starter includes helpers to handle the conversion:
import { latLngToOL, olToLatLng } from "@/lib/utils/coordinates";
// Convert [lat, lng] to OpenLayers coordinate
const olCoord = latLngToOL([51.505, -0.09]);
// Convert back
const latLng = olToLatLng(olCoord);
Proper Layer Management
OpenLayers requires explicit layer add/remove. Hooks are provided to simplify this:
const { tileProvider } = useMapTileProvider();
;
Memory‑Leak Prevention
OpenLayers needs manual cleanup. Every component disposes of resources correctly:
useEffect(() => {
// ... map initialization
return () => {
map.setTarget(undefined); // Detach from DOM
map.dispose(); // Dispose of resources
};
}, []);
Example Components
Map Page
"use client";
import { MapProvider } from "@/contexts/MapContext";
import { OpenLayersMap, OpenLayersTileLayer } from "@/components/map";
export default function MapPage() {
return (
);
}
GeoJSON Layer
import { OpenLayersGeoJSON } from "@/components/map";
;
Map Controls
import { useMapControls } from "@/hooks/useMapControls";
function MapControls() {
const { zoomIn, zoomOut, resetView, flyTo } = useMapControls();
return (
Zoom In
Zoom Out
Reset
flyTo([51.505, -0.09], 13)}>Fly to London
);
}
POI Panel
import { usePOIManager } from "@/hooks/usePOIManager";
function POIPanel() {
const { pois, addPOI, deletePOI, exportGeoJSON } = usePOIManager();
const handleAdd = () => {
addPOI("Coffee Shop", 51.505, -0.09, "food‑drink", "Great espresso");
};
return (
Add POI
Export
{pois.map((poi) => (
{poi.title}
deletePOI(poi.id)}>Delete
))}
);
}
Measurement Panel
import { useMeasurement } from "@/hooks/useMeasurement";
function MeasurementPanel() {
const { startMeasurement, clearMeasurement, distance, area } = useMeasurement();
return (
startMeasurement("distance")}>Measure Distance
startMeasurement("area")}>Measure Area
Clear
{distance > 0 &&
Distance: {(distance / 1000).toFixed(2)} km
}
{area > 0 &&
Area: {(area / 1000000).toFixed(2)} km²
}
);
}
Installation
# Clone the repository
git clone https://github.com/wellywahyudi/nextjs-openlayers-starter.git
cd nextjs-openlayers-starter
# Install dependencies
npm install
# Start development server
npm run dev
Open http://localhost:3000/map and you’re ready to go.
Configuration
Default Map Settings (constants/map-config.ts)
export const DEFAULT_MAP_CONFIG: MapConfig = {
defaultCenter: [51.505, -0.09], // [lat, lng]
defaultZoom: 13,
minZoom: 3,
maxZoom: 18,
};
Tile Providers (constants/tile-providers.ts)
export const TILE_PROVIDERS: TileProvider[] = [
{
id: "custom",
name: "My Custom Tiles",
url: "https://your-tile-server/{z}/{x}/{y}.png",
attribution: "© Your Attribution",
maxZoom: 19,
category: "standard",
},
// ...existing providers
];
Who Is This Starter For?
- 🗺️ Building a GIS application (spatial analysis, custom projections, vector tiles)
- 📊 Rendering large datasets (10 000+ features, complex geometries)
- 🎯 Need advanced interactions (drawing, editing, snapping, measurement)
- 🚀 Prototyping a map‑heavy app (dashboards, analytics, visualizations)
- 📚 Learning OpenLayers and want a clean, modern starting point
If you only need a simple map with markers, the Leaflet version is simpler. For power and flexibility, this OpenLayers starter is the right choice.
OpenLayers Specifics
- Projection handling – OpenLayers defaults to EPSG:3857 (Web Mercator) and expects
[lng, lat]. The starter automatically converts external[lat, lng]inputs:
// External API uses [lat, lng]
const userInput = [51.505, -0.09];
// Convert to OpenLayers format
const olCoord = latLngToOL(userInput);
// Use with OpenLayers
map.getView().setCenter(olCoord);
-
Layer order (bottom → top):
- Base Tile Layer
- GeoJSON Vector Layer
- POI Vector Layer
- Marker Vector Layer
- Measurement Vector Layer
-
Tree‑shakable imports:
// ✅ Good – tree‑shakeable
import Map from "ol/Map";
import View from "ol/View";
import TileLayer from "ol/layer/Tile";
// ❌ Bad – imports everything
import * as ol from "ol";
- Bundle size: Leaflet ~50 KB, OpenLayers ~90 KB. The increase is justified by the additional GIS features.
Roadmap
| Starter | Status |
|---|---|
| Next.js + Leaflet | ✅ Available |
| Next.js + OpenLayers | ✅ Available |
| Next.js + MapLibre GL | 🚧 Planned |
Same UI, same developer experience, different mapping libraries—pick the one that fits your project.
Contributing
This is open source (MIT license). If you find bugs, have ideas, or want to contribute:
- 🐛 Open an issue
- 💡 Start a discussion
- 🔧 Submit a PR
The codebase is clean, documented, and beginner‑friendly.