How to connect an AI-generated Next.js form to a backend
Connect a generated form to an authenticated same-origin API, save data for the right user, and verify it survives a reload.
Contents2 sections
A form that looks finished may still keep its values only in React state. To make it useful, connect its submit action to a same-origin API, validate and authorize the request on the server, and save the result in a database. Then load the saved data again when the page opens.
AI tools can generate backend code too: inspect what your project already contains before adding another endpoint. The example below adds a small form to CoreMVP’s existing Projects feature; it does not create a new backend or table.
Connect a form to the included Projects API
This example creates a Project in the currently selected Organization and loads its first page again after a reload. It uses the template's maintained Projects hooks, which key the list by user and Organization. The account must be an owner or admin. No backend feature is added.
Save this as in the Next.js template. Import ProjectForm in and place <ProjectForm /> inside LiveProjectsList's <DemoDashboardPage>, alongside the existing list. Open the signed-in /dashboard/projects page and choose an Organization using its existing selector. Remove this extra example after experimenting if the existing Projects form meets your needs.
"use client";
import { useState, type FormEvent } from "react";
import { useOrganizationSelection } from "@/components/dashboard/organization-selection";
import { useProjectMutation, useProjects } from "@/hooks/projects/use-projects";
export function ProjectForm() {
const { organization, loading, error } = useOrganizationSelection();
if (loading) return <p>Loading organization…</p>;
if (error || !organization) return <p>Choose an organization first.</p>;
return <SelectedProjectForm key={organization.id} organizationId={organization.id} />;
}
function SelectedProjectForm({ organizationId }: { organizationId: string }) {
const [name, setName] = useState("");
const [message, setMessage] = useState("");
const projects = useProjects(organizationId, false, null);
const create = useProjectMutation();
async function submit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setMessage("");
try {
await create.mutateAsync({ body: { organizationId, name } });
setName("");
setMessage("Project saved. Check the list below; if it failed to load, reload the page before trying again.");
} catch {
setMessage("Save could not be confirmed. Check the list before retrying.");
}
}
return (
<section>
<form onSubmit={submit}>
<label htmlFor="example-project-name">Project name</label>
<input id="example-project-name" value={name} onChange={(e) => setName(e.target.value)} />
<button disabled={create.isPending}>{create.isPending ? "Saving…" : "Save"}</button>
</form>
<p role="status">{message}</p>
{projects.isError && <p>Could not reload projects. Reload the page before retrying a save.</p>}
<ul>{projects.data?.data.map((project) => <li key={project.id}>{project.name}</li>)}</ul>
</section>
);
}The keyed form resets its messages when the selection changes. useProjects uses separate query keys for the signed-in user and Organization, so a late response for an earlier selection does not replace the current list. useProjectMutation invalidates the list after creation. A successful creation and a later list-load failure are reported separately: check the list before retrying to avoid creating a duplicate.
The GET returns at most the first page; use the included Projects hook and its cursor for pagination. The POST accepts a trimmed name of 1–80 characters and returns 201; invalid input returns 400. Hono validates the body, then the service verifies the actor and reloads their Organization role before the repository writes. A browser-supplied Organization ID is a target, not proof of permission. See Projects and the backend guide.
If the save succeeds but the subsequent reload fails, the record may already exist: check the list before retrying. For a different product record, define its persistence and access rules in your own service and repository. Keep credentials on the server and add an authorized read path as well as the write.
Verify the whole path
Use disposable data and two separately signed-in accounts. Save a distinctive value as the first user, reload the page, and confirm the value comes back from the server. Try invalid input and an anonymous request; neither should create or change a record. Then try reading and changing the Project as a signed-in user outside that Organization; confirm both are denied and the original value remains unchanged. Separately, test a member who can read but lacks the role needed to create or change a Project.
If the UI says success but a reload loses the value, inspect the API response and the authorized read path before changing the form. If the request fails, inspect the server-side validation and authorization result without exposing credentials or private data in browser errors. The API authorization test shows why checking stored data after a rejected write matters.