Next.js Example

A Next.js 15 application with React 19 that renders a MapLibre vector tile map and adds places search. Uses App Router route handlers as a server-side token proxy.

Prerequisites

Quick Start

git clone https://github.com/geogdev/examples.git
cd examples/frameworks/nextjs-maplibre
npm install
cp .env.local.example .env.local

Add your API key to .env.local:

GEOG_API_KEY=your-api-key-here

Start the dev server:

npm run dev

Open http://localhost:3000.

Project Structure

FileDescription
app/api/geog-token/route.tsRoute handler — exchanges API key for short-lived token, caches at 80% TTL
app/api/places/search/route.tsRoute handler — proxies places search requests to the Geog API
lib/geog.tsTyped API client with token exchange and places search functions
app/components/Map.tsxClient component — MapLibre map with transformRequest for auth
app/components/PlacesSearch.tsxClient component — search input, results list, and map markers
app/page.tsxHome page composing Map and PlacesSearch
.env.local.exampleEnvironment variable template

How It Works

Server-Side Token Proxy

The API key stays server-side in .env.local. Next.js App Router route handlers act as the proxy:

// app/api/geog-token/route.ts
let cachedToken: TokenResponse | null = null;
let refreshAt: number = 0;

export async function GET() {
  const now = Date.now();
  if (cachedToken && now < refreshAt) {
    return NextResponse.json({
      access_token: cachedToken.access_token,
      expires_in: cachedToken.expires_in,
    });
  }
  cachedToken = await exchangeToken(process.env.GEOG_API_KEY!);
  refreshAt = now + cachedToken.expires_in * 1000 * 0.8;
  return NextResponse.json({
    access_token: cachedToken.access_token,
    expires_in: cachedToken.expires_in,
  });
}

Client Components

Map and search components use the "use client" directive. The map component fetches a token from the local route handler, then uses MapLibre's transformRequest to inject the Bearer header:

// app/components/Map.tsx
"use client";

const mapRef = useRef<maplibregl.Map | null>(null);
const tokenRef = useRef<string>("");

useEffect(() => {
  fetch("/api/geog-token")
    .then((r) => r.json())
    .then((data) => {
      tokenRef.current = data.access_token;

      mapRef.current = new maplibregl.Map({
        transformRequest: (url) => {
          if (url.startsWith("https://api.geog.dev")) {
            return {
              url,
              headers: { Authorization: `Bearer ${tokenRef.current}` },
            };
          }
        },
        // ...
      });
    });
}, []);

Full Source

github.com/geogdev/examples/tree/main/frameworks/nextjs-maplibre

See Also