Home Quizzes Leaderboard Competitions Learn Hire Us
About Contact
Log In Sign Up
Learn Next.js Forms & Server Actions

Forms & Server Actions

⏱ 18 min read read
Server Actions --- Forms Without API Routes:

Server Actions let you run server-side code directly from a form submit
--- no API route needed. Mark a function with 'use server'.

// app/actions.ts

'use server';

import { revalidatePath } from 'next/cache';

export async function createStudent(formData: FormData) {

const name = formData.get('name') as string;

const grade = Number(formData.get('grade'));

// Save to DB here...

await db.students.create({ name, grade });

revalidatePath('/students'); // refresh the page data

}

// app/students/page.tsx --- use directly in form

import { createStudent } from '@/app/actions';

export default function Page() {

return (

<form action={createStudent}>

<input name="name" placeholder="Name" />

<input name="grade" type="number" />

<button type="submit">Add Student</button>

</form>

);

}

React Hook Form + Zod --- Client-Side Validation:

import { useForm } from 'react-hook-form';

import { zodResolver } from '@hookform/resolvers/zod';

import { z } from 'zod';

const schema = z.object({

name: z.string().min(2, 'Min 2 characters'),

email: z.string().email('Invalid email'),

grade: z.number().min(0).max(100),

});

type FormData = z.infer<typeof schema>;

const { register, handleSubmit, formState: { errors } } =
useForm<FormData>({

resolver: zodResolver(schema),

});

Server Actions are the Next.js 13+ way to handle mutations.

They work without JavaScript in the browser (progressive
enhancement).

Use useFormState and useFormStatus for pending/error feedback.

React Hook Form + Zod = best client-side form experience.

Install: npm i react-hook-form zod @hookform/resolvers
Code Example
'use client';

import { useForm } from 'react-hook-form';

import { zodResolver } from '@hookform/resolvers/zod';

import { z } from 'zod';

import { useState } from 'react';

const studentSchema = z.object({

name: z.string().min(2, 'Name must be at least 2 characters'),

email: z.string().email('Invalid email address'),

grade: z.coerce.number().min(0, 'Min 0').max(100, 'Max 100'),

subject: z.enum(['Math', 'English', 'Science'], { message:
'Pick a subject' }),

});

type StudentForm = z.infer<typeof studentSchema>;

export default function AddStudentForm() {

const [submitted, setSubmitted] = useState<StudentForm |
null>(null);

const {

register,

handleSubmit,

reset,

formState: { errors, isSubmitting },

} = useForm<StudentForm>({ resolver: zodResolver(studentSchema) });

const onSubmit = async (data: StudentForm) => {

await new Promise(r => setTimeout(r, 500)); // simulate API call

setSubmitted(data);

reset();

};

return (

<form onSubmit={handleSubmit(onSubmit)} style={{ display: 'flex',
flexDirection: 'column', gap: 12 }}>

<div>

<input {...register('name')} placeholder="Full name" />

{errors.name && <p style={{ color: 'red'
}}>{errors.name.message}</p>}

</div>

<div>

<input {...register('email')} placeholder="Email"
type="email" />

{errors.email && <p style={{ color: 'red'
}}>{errors.email.message}</p>}

</div>

<div>

<input {...register('grade')} placeholder="Grade (0--100)"
type="number" />

{errors.grade && <p style={{ color: 'red'
}}>{errors.grade.message}</p>}

</div>

<select {...register('subject')}>

<option value="">Select subject</option>

<option value="Math">Math</option>

<option value="English">English</option>

<option value="Science">Science</option>

</select>

{errors.subject && <p style={{ color: 'red'
}}>{errors.subject.message}</p>}

<button type="submit" disabled={isSubmitting}>

{isSubmitting ? 'Saving...' : 'Add Student'}

</button>

{submitted && <p style={{ color: 'green' }}>Added:
{submitted.name} ({submitted.grade})</p>}

</form>

);

}
← Hooks --- useEffect, useContext & Custom Databases with Prisma →

Log in to track your progress and earn badges as you complete lessons.

Log In to Track Progress