Skip to main content

Working with Forms

API Routes

Next.js allows you to create API routes where you can define server-side functions to handle data fetching and mutations. API routes are typically created inside the pages/api directory, and each file within this directory defines a separate route.

These routes can handle requests, perform operations like database queries, and return data to the client.

pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, world!' });
}

Server-Only Forms

You need to create certain API-endpont to handle recieved data on the server.

api/submit.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const data = req.body;
const id = await createItem(data);
res.status(200).json({ id });
}

Then create the form and handle the submission.

import { FormEvent } from 'react';

export default function Page() {
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();

const formData = new FormData(event.currentTarget);
const response = await fetch('/api/submit', {
method: 'POST',
body: formData,
});

// Handle response if necessary
const data = await response.json();
// ...
}

return (
<form onSubmit={onSubmit}>
<input type="text" name="name" />
<button type="submit">Submit</button>
</form>
);
}

Data Validation

If you need to validate the data on client, you can use HTML attibutes like required and type=email.

For more complex data, you may consider validate them on server using schema validation libraries like zod.

Here is updated api endpoint.

api/submit.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';

const schema = z.object({
// ...
});

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const parsed = schema.parse(req.body);
// ...
}

Error Handling

You can use React state to set an error message if form submission request fails.

import { useState } from 'react';

function RegisterForm() {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [isSuccess, setSuccess] = useState<boolean>(false);

const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setLoading(true);
setError(null);
setIsSuccess(false);

try {
const formData = new FormData(event.currentTarget);
const response = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: formData,
});

if (!response.ok) {
const { error } = await response.json();
setError(error);
return;
}

setIsSuccess(true);
} catch (error) {
setError('Something went wrong');
}

setLoading(false);
};

return (
<form onSubmit={handleSubmit}>
{error && <p>{error}</p>}
{isSuccess && <p>Registration successful!</p>}
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
/>
<button type="submit" disabled={isLoading}>
Register
</button>
</form>
);
}

export default RegisterForm;