Next.js Integration
Integrate UI Bridge with Next.js using App Router API routes.
Installation
npm install @qontinui/ui-bridge @qontinui/ui-bridge-server
API Routes Setup
App Router (Next.js 13+)
Create the catch-all route handler. createNextRouteHandlers takes the handler
set you build once (see Server Overview) and
returns the three route exports the App Router expects:
import { createNextRouteHandlers } from '@qontinui/ui-bridge-server/nextjs';
import { handlers } from '@/lib/ui-bridge';
export const { GET, POST, DELETE } = createNextRouteHandlers(handlers);
import { getGlobalRegistry, createActionExecutor } from '@qontinui/ui-bridge';
import { createHandlers } from '@qontinui/ui-bridge-server';
const registry = getGlobalRegistry();
export const handlers = createHandlers(registry, createActionExecutor(registry));
Zero-config variant
If you only need the route surface to exist — endpoint discovery, health, and
well-formed empty responses — createUIBridgeHandler builds the whole thing
for you and returns a single handler:
import { createUIBridgeHandler } from '@qontinui/ui-bridge-server/nextjs';
const handler = createUIBridgeHandler();
export const GET = handler;
export const POST = handler;
export const DELETE = handler;
createUIBridgeHandler wires server-safe stub implementations: read
operations return empty data and write operations return an error. Your
browser's live elements are not reachable through it. For real control, pass
your own handlers to createNextRouteHandlers — relay-backed ones if the UI
runs in the browser, as described in
Standalone Server.
Provider Setup
import { UIBridgeProvider } from '@qontinui/ui-bridge';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<UIBridgeProvider
config={{
apiPath: '/api/ui-bridge',
}}
>
{children}
</UIBridgeProvider>
</body>
</html>
);
}
Health check and route discovery
Both are served by the catch-all itself, so they need no handler entry:
curl http://localhost:3000/api/ui-bridge/health
# {"status":"ok","timestamp":1234567890}
curl http://localhost:3000/api/ui-bridge/_routes
# {"success":true,"data":{"routes":[...],"count":50},"timestamp":...}
/health returns the same { status, timestamp } the
standalone and Express adapters do.
The path differs by construction: those two serve it at the server root, while
a Next route handler is only reachable through its mount, so here it sits
inside the catch-all.
Route params across the supported Next versions
App Router params is a plain object on Next 13.0–13.3 and a Promise from
13.4 onward — it is a Promise on 15 and 16. Every handler this package
exports accepts either shape and awaits internally, so the recipes above
compile against the whole ^13 || ^14 || ^15 || ^16 peer range without a
version-specific wrapper.
This matters more than it sounds: next build runs Next's own generated route
validator over your exported handlers, so a handler typed against only one of
the two shapes is a build failure, not a warning. examples/nextjs-app in
the repo is the recipe above, built by CI, which is what keeps that claim
honest.
Pages Router (Legacy)
There is no Pages Router adapter. @qontinui/ui-bridge-server/nextjs exports
App Router handlers only — createNextRouteHandlers, createUIBridgeHandler,
createRenderLogHandlers, createControlHandlers and createDebugHandlers,
all of which speak the Web Request/Response API rather than
NextApiRequest/NextApiResponse.
On Next.js 12 or a Pages-Router-only app, mount the Express adapter on a custom server, or run the standalone server as a sidecar.
Per-domain handlers
Instead of one catch-all, you can mount narrower routes.
createControlHandlers(handlers) and createDebugHandlers(handlers) return an
object of route modules keyed by endpoint (elements, element,
components, … and actionHistory, metrics, highlight):
import { createDebugHandlers } from '@qontinui/ui-bridge-server/nextjs';
import { handlers } from '@/lib/ui-bridge';
export const { GET } = createDebugHandlers(handlers).metrics;
createRenderLogHandlers(handlers) covers a single endpoint, so it returns the
route methods directly:
import { createRenderLogHandlers } from '@qontinui/ui-bridge-server/nextjs';
import { handlers } from '@/lib/ui-bridge';
export const { GET, DELETE } = createRenderLogHandlers(handlers);
Configuration
NextJSAdapterConfig extends UIBridgeServerConfig with a runtime field:
| Option | Type | Description |
|---|---|---|
runtime | 'edge' | 'nodejs' | Declared runtime hint |
authenticate | (req) => boolean | Promise<boolean> | Runs before every route; falsy returns 401 |
basePath | string | Route prefix |
cors | boolean | CORSOptions | Declared on the base config |
With Authentication
Pass authenticate rather than wrapping the exported handlers — it runs inside
the adapter, ahead of route matching:
import { createNextRouteHandlers } from '@qontinui/ui-bridge-server/nextjs';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { handlers } from '@/lib/ui-bridge';
export const { GET, POST, DELETE } = createNextRouteHandlers(handlers, {
authenticate: async () => {
const session = await getServerSession(authOptions);
return session?.user?.isAdmin === true;
},
});
A failed check returns 401 with the standard
{ success: false, error: 'Unauthorized', code: 'UNAUTHORIZED' } envelope.
Development Only
import { createNextRouteHandlers } from '@qontinui/ui-bridge-server/nextjs';
import { handlers } from '@/lib/ui-bridge';
export const { GET, POST, DELETE } = createNextRouteHandlers(handlers, {
authenticate: () => process.env.NODE_ENV === 'development',
});
Using Components
Register UI elements in your pages:
'use client';
import { useUIElement, useUIComponent } from '@qontinui/ui-bridge';
import { useState } from 'react';
export default function HomePage() {
const [count, setCount] = useState(0);
const button = useUIElement({
id: 'counter-button',
type: 'button',
});
useUIComponent({
id: 'counter',
name: 'Counter Component',
actions: [
{
id: 'increment',
handler: async () => {
setCount((c) => c + 1);
return { count: count + 1 };
},
},
{
id: 'reset',
handler: async () => {
setCount(0);
return { count: 0 };
},
},
],
});
return (
<div>
<p>Count: {count}</p>
<button ref={button.ref} data-ui-id="counter-button" onClick={() => setCount((c) => c + 1)}>
Increment
</button>
</div>
);
}
Server Components
UI Bridge hooks require client components. Use the 'use client' directive:
// This won't work in a Server Component
// 'use client' is required
'use client';
import { useUIElement } from '@qontinui/ui-bridge';
export function InteractiveButton() {
const control = useUIElement({ id: 'my-button', type: 'button' });
return <button ref={control.ref}>Click me</button>;
}
Edge Runtime
UI Bridge is compatible with Edge Runtime:
import { createUIBridgeHandler } from '@qontinui/ui-bridge-server/nextjs';
export const runtime = 'edge';
const handler = createUIBridgeHandler({ runtime: 'edge' });
export const GET = handler;
export const POST = handler;
Middleware
Add UI Bridge to your middleware for request logging:
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/api/ui-bridge')) {
// Log UI Bridge requests
console.log(`UI Bridge: ${request.method} ${request.nextUrl.pathname}`);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/ui-bridge/:path*',
};
Complete Example
See the Next.js Example App for a full working example.