← All posts Developers

Address autocomplete in a Next.js App Router form

1 Aug 2026 · 6 min read

The App Router makes one address-form decision for you: autocomplete is interactive, so it lives in a client component. What is less obvious is which key belongs where, and how to verify an address on the server without being billed for the same lookup twice.

The field itself, in a client component

'use client';
import { AddressAutocomplete } from '@wattleaddr/react';

export function ShippingAddress({ onPick }: { onPick: (a: any) => void }) {
  return (
    <AddressAutocomplete
      publishableKey={process.env.NEXT_PUBLIC_WATTLEADDR_KEY!}
      onSelect={(address) => onPick(address)}
      inputProps={{ name: 'address', autoComplete: 'off', placeholder: 'Start typing an address' }}
    />
  );
}

Install it with `npm install @wattleaddr/react`. The component handles the request lifecycle — debouncing, ordering, and the session token that ties the keystrokes to the final selection.

Why the publishable key in NEXT_PUBLIC_ is fine

Anything named `NEXT_PUBLIC_*` is inlined into the client bundle at build time. That is exactly right for a publishable key (`waddr_pk_…`), which is designed to be visible and is locked to the domains you nominate — lifted onto someone else’s site, it stops working.

It is exactly wrong for a secret key (`waddr_sk_…`). Never give one a `NEXT_PUBLIC_` name: the build bakes it into JavaScript that every visitor downloads, and rotating it afterwards means a redeploy. A secret key belongs in a server-only environment variable, read inside a route handler or a server action.

One session, one billable lookup

Autocomplete requests are not billed individually. A `session` token groups all the keystrokes for a single address search with the retrieve that follows, and the whole session counts as one lookup. The React component and the JS SDK manage the token for you — this matters mainly when you call the REST API directly, where forgetting it turns one address into a dozen billable events.

The practical rule: one session token per address the user is searching for. Start a new one when they clear the field and begin again.

Verifying on the server, without paying twice

Verify exists for free text you already hold — a CSV import, a legacy record, an address pasted into a support ticket. It is not a second check on something the user just picked from the dropdown: that record came straight out of G-NAF and has already been paid for.

// app/api/verify-address/route.ts — server-only, secret key never reaches the browser
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const { address } = await req.json();
  const res = await fetch('https://api.wattleaddr.com.au/v1/addresses/verify', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.WATTLEADDR_SECRET_KEY}`,  // no NEXT_PUBLIC_
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ address }),
  });
  const data = await res.json();
  return NextResponse.json({
    matched: data.matched,
    confidence: data.confidence,        // high | medium | low
    address: data.match,                // null when nothing matched
  });
}

Handle "no match" as a real outcome

A verify that fails to match is not an error — the API returns 200 with `matched: false`, and it is not billed. Treat it as a branch in your form, not an exception: let the customer proceed with what they typed and flag the record for review. Blocking checkout because an address is missing from a quarterly dataset costs more than the bad row does.

The same applies to PO Boxes. They are not in G-NAF at all, so the API returns a `postal_address_unsupported` notice rather than pretending. Show it, and give people a separate field for a postal address.

Where the requests actually go

Both the browser calls and your server calls terminate on Australian infrastructure. Nothing is routed through a foreign CDN or an ad platform on the way, which is usually the point for anyone who has had to answer a data-residency question about their checkout.

Get an API key Read the docs