Add AutoSugges autocomplete to a Next.js App Router project
Keep the input in a Client Component and the page around it a Server Component, so only the interactive part ships JavaScript.
How do I add autocomplete to a Next.js App Router page?
Put the input in its own Client Component marked use client, create the client there with the runtime URL from a NEXT_PUBLIC_ variable and your publishable key, and render Autocomplete from @autosugges/client/react. The surrounding page stays a Server Component, so only the input ships JavaScript to the browser.
What you will have
A working autocomplete inside a Next.js App Router page, with the runtime origin supplied by a public environment variable and the surrounding page left as a Server Component.
Before you start
- An AutoSugges list that has been published at least once. An unpublished list has nothing to serve yet and returns
list_not_published. - The publishable key issued for the application that will query it.
- The runtime base URL for the environment you are targeting. AutoSugges does not have one fixed public hostname baked into the SDK — the dashboard's integration panel shows the origin for your environment.
- Next.js 14 or newer, App Router.
Values you supply
| Value | Placeholder | Where it comes from |
|---|---|---|
| baseUrlRequired · public by design | YOUR_RUNTIME_BASE_URL | The AutoSugges dashboard's integration panel, for the environment you are deploying to. The origin of the AutoSugges runtime — scheme and host, no trailing slash and no path. The SDK appends /v1/... itself. |
| publishableKeyRequired · public by design | YOUR_PUBLISHABLE_KEY | The AutoSugges dashboard, under the application that will make the queries. Identifies the consumer, the application, the subscription, the canonical list and the query policy in a single server-side lookup. The client never supplies a list id, tenant id or version hash — if a generated integration is passing one of those, it is wrong. |
| accessTokenOptional · never in client code | YOUR_SERVER_MINTED_ACCESS_TOKEN | Your own backend, which mints it from your AutoSugges credentials. Only for a list whose policy requires a token. Short-lived, sent as authorization: Bearer <token>, and never minted, stored or hardcoded in client code. |
Steps
- Isolate the interactive part. Put the input in its own file marked "use client". It holds changing state; nothing else about the page needs to.
- Supply the base URL from the environment. Read the runtime origin from a NEXT_PUBLIC_-prefixed variable so it reaches the browser bundle. Declare it in .env.local and in your deployment environment.
- Create the client and bootstrap. createAutosuggesClient and bootstrap() behave identically here and in plain React — Next.js changes where code runs, not the SDK API.
- Render the component. Import Autocomplete from @autosugges/client/react inside the Client Component only.
- Keep the page a Server Component. Import the Client Component from an ordinary server page. Do not add "use client" to the page itself.
Install
npm install @autosugges/client
Code
app/search/city-field.tsx
// app/search/city-field.tsx
// A Client Component, because it holds interactive state. That is the only
// reason — the SDK itself has no opinion about where it runs.
'use client';
import { useState } from 'react';
import { createAutosuggesClient } from '@autosugges/client';
import { Autocomplete } from '@autosugges/client/react';
const autosugges = createAutosuggesClient({
// Must be NEXT_PUBLIC_-prefixed to reach the browser bundle. The publishable
// key may be inlined the same way: it is public by design.
baseUrl: process.env.NEXT_PUBLIC_AUTOSUGGES_RUNTIME_URL!,
publishableKey: 'YOUR_PUBLISHABLE_KEY',
});
void autosugges.bootstrap().catch(() => {});
export function CityField() {
const [city, setCity] = useState('');
const [errorCode, setErrorCode] = useState<string | undefined>();
return (
<>
<Autocomplete
source={autosugges}
label="City"
onSelect={(item) => setCity(item.value)}
onError={(error) => setErrorCode(error.code)}
/>
<input type="hidden" name="city" value={city} />
{errorCode !== undefined && <p role="alert">Lookup unavailable ({errorCode}).</p>}
</>
);
}
// app/search/page.tsx — stays a Server Component. Nothing about AutoSugges
// needs to run on the server here.
import { CityField } from './city-field';
export default function SearchPage() {
return (
<main>
<h1>Find a city</h1>
<CityField />
</main>
);
}Security
- A NEXT_PUBLIC_ variable is embedded in the browser bundle. Put only the runtime base URL and the publishable key there — never a secret key, a service-role credential or a database URL.
Check that it works
- Run the dev server and confirm the page renders without a "useState in a Server Component" error — if it appears, the "use client" boundary is in the wrong file.
- Confirm in the network panel that requests go to the origin your NEXT_PUBLIC_ variable holds, and that the
keytravels as a query parameter. - Build for production and confirm the env var is present at build time: an undefined base URL produces a request to
undefined/v1/query. - Type a query and confirm exactly one
/v1/queryrequest per settled input. - Operate the control with the keyboard only.
The search page server-renders, the input hydrates as the only client-side island, and typing returns suggestions from the published list.
Try it live
Paste a publishable key from one of your published lists to run a real query against this environment’s runtime — the same @autosugges/client the code above uses.
A published list's publishable key — a public identifier, safe to paste here.
Paste a publishable key to try a live query.
Notes
- Never hardcode a minimum query length or a debounce interval. Both come from
client.policy, which the SDK fills from the bootstrap response and updates in place. The constants the SDK falls back to before the first successful bootstrap are documented defaults, not the list’s real policy. - Render
displayValue, storevalue. They differ:displayValueis resolved for the requested locale at compile time. - A selection report is optional. Omitting it costs ranking quality over time and nothing else; it is never required for a query to work.
Recipes for specific datasets
The same integration, specialised to the datasets whose behaviour changes the code.
Frequently asked questions
- What is the minimum number of characters before a query runs?
- Two by default. A list owner can change the minimum, but it can never be 1. The client reads the real value from bootstrap and sends no request below it, so do not hardcode it.
- How long does the client wait after typing before it queries?
- 150 milliseconds by default. The value is delivered through bootstrap and a list owner can raise it, so the client uses the policy value rather than a constant.
- Where does the publishable key go?
- In the key query parameter of the request, for example /v1/query?key=YOUR_PUBLISHABLE_KEY, never in a header. The key is public by design and safe in client code. Secret keys never belong in a browser or mobile bundle, and a proxy to hide the publishable key is unnecessary.
- What happens when my query quota runs out?
- The runtime returns a structured quota_exceeded response that carries no items and no placeholder suggestions, so exhaustion is never mistaken for no results. Render it from the typed error code and prompt an upgrade.
- Which values belong in NEXT_PUBLIC_ variables?
- Only the runtime base URL and the publishable key. NEXT_PUBLIC_ values are compiled into the browser bundle, so never put a secret key, a service-role credential or a database URL there.