The user-facing side of Create is a form. Done well, it validates early, handles errors, and never double-submits.
1"use client";
2import { useState } from "react";
3
4export function NewTaskForm({ onCreated }: { onCreated: (t: Task) => void }) {
5 const [title, setTitle] = useState("");
6 const [submitting, setSubmitting] = useState(false);
7 const [error, setError] = useState<string | null>(null);
8
9 async function handleSubmit(e: React.FormEvent) {
10 e.preventDefault();
11 setSubmitting(true);
12 setError(null);
13 try {
14 const res = await fetch("/api/v1/tasks", {
15 method: "POST",
16 headers: { "Content-Type": "application/json" },
17 body: JSON.stringify({ title }),
18 });
19 if (!res.ok) {
20 const body = await res.json();
21 throw new Error(body.error?.message ?? "Failed to create task");
22 }
23 const { data } = await res.json();
24 onCreated(data);
25 setTitle("");
26 } catch (err) {
27 setError((err as Error).message);
28 } finally {
29 setSubmitting(false);
30 }
31 }
32
33 return (
34 <form onSubmit={handleSubmit}>
35 <input value={title} onChange={(e) => setTitle(e.target.value)} required />
36 <button disabled={submitting || !title.trim()}>
37 {submitting ? "Saving…" : "Add task"}
38 </button>
39 {error && <p role="alert">{error}</p>}
40 </form>
41 );
42}Client validation is for UX (instant feedback). Server validation is for safety (it's the only one an attacker can't bypass). Do both — reuse the same Zod schema on both sides if you can.
In Next.js you can post a form straight to a server function — progressive enhancement, works without JS:
1<form action={createTask}>
2 <input name="title" required />
3 <button>Add</button>
4</form>