π Technical Briefing: This tutorial is part of our deep-dive series on Agentic Workflows at Gate of AI. For the full technical breakdown, interactive code sandbox, and the native Arabic translation, visit the original article here.
Tutorial
Build a Next.js Support Workflow for an OpenAI Backend
Create a focused customer-support intake and review interface with Next.js App Router, TypeScript, and a server route boundary that is ready for a separately verified AI service.
By the Gate of AI Editorial & Engineering Teams, GateOfAI, LLC.
What this verified tutorial builds
This tutorial builds a small support-workflow application using Next.js and React. An agent can enter a customer message, choose a support category, select a reply style, submit the request to a server route, and review the normalized result. The completed project is intentionally useful without pretending that a language model has been connected when its provider contract has not been verified.
The verified context supports Next.js as a React framework used to build modern web applications. It identifies capabilities including App Router, server-side rendering, static generation, API routes, TypeScript support, and image optimization. This guide uses the App Router and a route handler because they provide a clear separation between an interactive browser interface and server-side application logic.
The verified OpenAI context also describes an OpenAI DevDay example in which a FastAPI server was connected to a Next.js front end. That example is an important architectural lesson: a Next.js interface can work with a dedicated backend service. It does not, however, verify a particular OpenAI JavaScript SDK, model name, endpoint, parameter, response schema, price, or quota. For that reason, this article creates a dependable integration boundary rather than publishing unverified provider code.
The result is a foundation for a support product: a browser form, a typed request contract, a server route, a predictable response shape, and a review panel. Once your organization has verified the current OpenAI API documentation and its own handling requirements for customer data, the sample route can be replaced with a server-side adapter that calls the approved provider service.
Prerequisites
- A current Node.js LTS installation and npm.
- Basic familiarity with React components, TypeScript, and the command line.
- A new Next.js project created with TypeScript and App Router enabled.
- A code editor capable of editing TypeScript and TSX files.
- Non-sensitive example support messages for local testing.
Do not use real passwords, access tokens, payment-card data, or confidential customer records in a local demonstration. This tutorial processes example text only. A production support workflow needs its own approved data-handling, retention, access-control, and review processes.
Step 1: Create the Next.js project
Create a new application with the current Next.js project generator. Select TypeScript and App Router when prompted. The exact generator prompts can change between releases, so review them before confirming your choices.
npx create-next-app@latest nextjs-support-workflow
cd nextjs-support-workflow
npm run dev
Open http://localhost:3000 after the development server starts. Next.js provides a file-based application structure. In this tutorial, the home page is rendered from src/app/page.tsx, while the server endpoint is implemented in src/app/api/support-request/route.ts.
Keep the initial application narrow. A support interface does not need authentication, a customer database, an AI provider, a CRM integration, or automatic sending in order to validate its first workflow. Start by making the request and review experience clear. Then add independently tested capabilities one at a time.
Step 2: Define a shared support contract
A support interface benefits from a stable contract between the browser and server. TypeScript provides compile-time feedback, but the route handler still checks incoming values at runtime. The browser is not a trusted enforcement point: a user can alter form values or send a request directly to the route.
Create src/lib/support.ts:
export const productAreas = [
"Billing",
"Account access",
"Technical issue",
"General question",
] as const;
export const tones = ["Warm", "Concise", "Formal"] as const;
export type ProductArea = (typeof productAreas)[number];
export type Tone = (typeof tones)[number];
export type SupportRequest = {
ticket: string;
productArea: ProductArea;
tone: Tone;
};
export type SupportReview = {
received: boolean;
ticketLength: number;
productArea: ProductArea;
tone: Tone;
nextStep: string;
};
export function isProductArea(value: unknown): value is ProductArea {
return typeof value === "string" && productAreas.includes(value as ProductArea);
}
export function isTone(value: unknown): value is Tone {
return typeof value === "string" && tones.includes(value as Tone);
}
export function parseSupportRequest(value: unknown): SupportRequest | null {
if (!value || typeof value !== "object") return null;
const input = value as Record<string, unknown>;
const ticket = typeof input.ticket === "string" ? input.ticket.trim() : "";
if (ticket.length < 20 || ticket.length > 8000) return null;
if (!isProductArea(input.productArea) || !isTone(input.tone)) return null;
return { ticket, productArea: input.productArea, tone: input.tone };
}
This module gives the form and route handler one vocabulary. The request contains ticket text, a bounded category, and a bounded tone. The response is deliberately operational rather than AI-generated: it confirms receipt, records the text length, repeats the selected options, and provides a next-step message. That makes the application testable without representing simulated content as an AI answer.
The 20-to-8,000-character range is a product decision in this sample, not an OpenAI limit. Adjust it after evaluating the types of messages your own support team receives and the constraints of any approved backend service.
Step 3: Add the server route handler
Route handlers provide a server-side HTTP boundary in an App Router project. Create src/app/api/support-request/route.ts:
import { NextRequest } from "next/server";
import {
parseSupportRequest,
type SupportReview,
} from "@/lib/support";
export async function POST(request: NextRequest) {
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json(
{ error: "Send a valid JSON request body." },
{ status: 400 },
);
}
const input = parseSupportRequest(body);
if (!input) {
return Response.json(
{
error:
"Enter 20 to 8,000 characters and select a supported product area and tone.",
},
{ status: 400 },
);
}
const review: SupportReview = {
received: true,
ticketLength: input.ticket.length,
productArea: input.productArea,
tone: input.tone,
nextStep:
"Request accepted for human review. Connect an approved server-side AI adapter only after its provider contract is verified.",
};
return Response.json({ review }, { status: 200 });
}
The route parses JSON, validates it with the shared helper, and returns either a controlled 400 response or a stable review object. This is valuable even before AI is introduced: the browser has a defined contract, and direct requests cannot bypass server-side input checks.
When a provider integration is approved, replace the construction of review with a call to a server-controlled adapter. Do not place provider credentials in browser code or variables intentionally exposed to the client. Keep the request adapter separate from the page component so that its behavior, error handling, logging policy, and tests can be reviewed independently.
If your future product is a ChatGPT App rather than a conventional web application, account for the official OpenAI guidance that Apps run in a double-nested iframe and can be subject to strict Content Security Policies. A local interface can behave differently from a deployed iframe integration, so validate the final deployment environment early.
Step 4: Build the client-side support form
Create src/components/support-workflow.tsx. The component is a client component because it handles form events and browser state.
"use client";
import { FormEvent, useState } from "react";
import {
productAreas,
tones,
type SupportReview,
} from "@/lib/support";
type FormState = {
ticket: string;
productArea: (typeof productAreas)[number];
tone: (typeof tones)[number];
};
const initialForm: FormState = {
ticket: "",
productArea: "General question",
tone: "Warm",
};
export function SupportWorkflow() {
const [form, setForm] = useState<FormState>(initialForm);
const [review, setReview] = useState<SupportReview | null>(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setError("");
setReview(null);
if (form.ticket.trim().length < 20) {
setError("Enter at least 20 characters of ticket text.");
return;
}
setLoading(true);
try {
const response = await fetch("/api/support-request", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(form),
});
const payload: unknown = await response.json();
if (!response.ok || !payload || typeof payload !== "object" || !("review" in payload)) {
const message =
payload && typeof payload === "object" && "error" in payload
? String(payload.error)
: "The server could not process this request.";
throw new Error(message);
}
setReview(payload.review as SupportReview);
} catch (caught) {
setError(caught instanceof Error ? caught.message : "Unexpected request failure.");
} finally {
setLoading(false);
}
}
return (
<section>
<form onSubmit={submit}>
<label htmlFor="ticket">Customer message</label>
<textarea
id="ticket"
value={form.ticket}
onChange={(event) => setForm((current) => ({ ...current, ticket: event.target.value }))}
minLength={20}
maxLength={8000}
rows={10}
required
/>
<label htmlFor="productArea">Product area</label>
<select
id="productArea"
value={form.productArea}
onChange={(event) => setForm((current) => ({
...current,
productArea: event.target.value as FormState["productArea"],
}))}
>
{productAreas.map((area) => <option key={area}>{area}</option>)}
</select>
<label htmlFor="tone">Requested reply style</label>
<select
id="tone"
value={form.tone}
onChange={(event) => setForm((current) => ({
...current,
tone: event.target.value as FormState["tone"],
}))}
>
{tones.map((tone) => <option key={tone}>{tone}</option>)}
</select>
{error ? <p role="alert">{error}</p> : null}
<button type="submit" disabled={loading}>
{loading ? "Submittingβ¦" : "Submit for review"}
</button>
</form>
<aside aria-live="polite">
<h2>Review status</h2>
{review ? (
<ul>
<li>Received: {review.received ? "Yes" : "No"}</li>
<li>Characters received: {review.ticketLength}</li>
<li>Product area: {review.productArea}</li>
<li>Requested style: {review.tone}</li>
<li>Next step: {review.nextStep}</li>
</ul>
) : (
<p>Submit a non-sensitive example to test the server contract.</p>
)}
</aside>
</section>
);
}
The component checks ticket length for fast feedback, while the route repeats validation for enforcement. After the asynchronous request, the component updates state only from the received response. The review panel uses aria-live so assistive technologies can announce status changes.
Step 5: Render the workflow from the App Router page
Replace src/app/page.tsx with the following:
import { SupportWorkflow } from "@/components/support-workflow";
export default function HomePage() {
return (
<main>
<h1>Support Workflow Review</h1>
<p>
A Next.js App Router example with a typed browser-to-server support request.
</p>
<SupportWorkflow />
</main>
);
}
Run the development server and submit an example message longer than 20 characters. You should receive a normalized review result. Then test invalid input, including a short message or a request with an unsupported category, to confirm that the route returns a controlled error.
Testing and production decisions
Before connecting an AI service, test the deterministic parts of the system. Confirm that valid input receives a 200 response, malformed JSON receives a 400 response, unsupported category values are rejected, and the browser displays a useful error. These checks are independent of any model and remain valuable after an AI adapter is added.
curl -i -X POST http://localhost:3000/api/support-request
-H "Content-Type: application/json"
-d '{"ticket":"I need help understanding a duplicate billing entry on my example account.","productArea":"Billing","tone":"Warm"}'
A future provider adapter should have explicit tests for its request construction, response parsing, timeout behavior, and safe error messages. Do not test a support workflow merely by checking whether it produces plausible text. Test whether the application preserves the selected category, handles failures predictably, keeps sensitive credentials server-side, and requires human review before consequential communication is sent.
Production deployment also requires decisions outside this tutorial: authentication, authorization, shared rate limiting, audit requirements, monitoring, redaction, retention, and incident response. Those choices depend on the organization and jurisdiction. Treat them as engineering and governance work, not as capabilities automatically supplied by a model call.
What to add after API verification
- An approved server-side AI adapter with provider documentation reviewed at implementation time.
- A narrow, validated output contract appropriate for your support operation.
- Authenticated user and organization identity for request controls.
- A human approval queue instead of automatic customer-facing sending.
- Source-aware policy retrieval only after policy ownership and data handling are defined.
- Deployment tests for Content Security Policy restrictions if the product will run as a ChatGPT App.
The practical lesson is straightforward: Next.js provides a capable React application foundation, while an AI provider should be treated as a separately verified server-side dependency. Establish the support workflow and its boundaries first. Then integrate the approved AI service with current official documentation, focused testing, and human accountability.
Sources
- OpenAI Developers: How Codex ran OpenAI DevDay 2025 β verified example of a FastAPI server connected to a Next.js front end.
- OpenAI Developers: 15 lessons learned building ChatGPT Apps β verified guidance on iframe isolation, Content Security Policy constraints, and development workflow considerations.
- Next.js Reviews (2026) β context describing App Router, React, server-side rendering, static generation, API routes, and TypeScript support.
Top comments (0)