Series 2: My Next.js 16 + OpenLayers + TypeScript Starter Kit — A Modern Map Apps Setup

Published: (December 5, 2025 at 10:58 AM EST)
4 min read
Source: Dev.to

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

TechnologyVersionPurpose
Next.js16App Router, Server Components
React19UI Framework
OpenLayers10Mapping (vanilla, no wrapper)
TypeScript5Type safety
Tailwind CSS4Styling
shadcn/uiLatestAccessible UI components

Features

  • Base map setup – OpenLayers Map and View with 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, localStorage persistence
  • 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):

    1. Base Tile Layer
    2. GeoJSON Vector Layer
    3. POI Vector Layer
    4. Marker Vector Layer
    5. 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

StarterStatus
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.

Back to Blog

Related posts

Read more »

AI-Powered Development Platform

🤔 The Problem That Kept Me Up at Night Picture this: You discover an awesome open‑source project on GitHub. It has 10,000+ issues, hundreds of contributors, a...