Next.js city autocomplete tutorial

Two ways to wire this up in a Next.js App Router project: a plain client component hitting the free, no-key autocomplete endpoint directly (simplest — no server involved), or a route handler that proxies the authenticated search API so your RapidAPI key never reaches the browser.

Live demo

Type a Japanese city, in Japanese or English:

Type to search Japan cities…

Option A: client component, free endpoint

// app/components/CityAutocomplete.tsx
'use client';

import { useState, useEffect } from 'react';

export function CityAutocomplete({ countryIso, lang }: { countryIso: string; lang: string }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState<any[]>([]);

  useEffect(() => {
    const prefix = query.trim().toLowerCase();
    if (!prefix) { setResults([]); return; }
    const timer = setTimeout(() => {
      fetch(`https://oneshot.geomelon.dev/${countryIso}/${lang}/${encodeURIComponent(prefix)}`)
        .then((res) => (res.ok ? res.json() : []))
        .then(setResults)
        .catch(() => setResults([]));
    }, 150);
    return () => clearTimeout(timer);
  }, [countryIso, lang, query]);

  return (
    <div>
      <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Start typing a city..." />
      <ul>
        {results.map((c) => (
          <li key={c.id}>{c.name}</li>
        ))}
      </ul>
    </div>
  );
}

Option B: route handler proxy, authenticated search API

Keeps your RapidAPI key server-side, and gives you sorting/pagination/multilingual params the free endpoint doesn't have:

// app/api/cities/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(req: NextRequest) {
  const { searchParams } = new URL(req.url);
  const name = searchParams.get('name') ?? '';
  const countryCode = searchParams.get('countryCode') ?? '';

  const res = await fetch(
    `https://geomelon.p.rapidapi.com/cities/search?name=${encodeURIComponent(name)}&countryCode=${countryCode}&sort=population_desc&limit=10`,
    {
      headers: {
        'x-rapidapi-host': 'geomelon.p.rapidapi.com',
        'x-rapidapi-key': process.env.GEOMELON_API_KEY!,
      },
    },
  );

  const cities = await res.json();
  return NextResponse.json(cities);
}

Then call /api/cities?name=par&countryCode=fr from your client component instead of the external URL directly — the key stays in an environment variable on the server, never shipped to the browser.

Get a free RapidAPI key