Reputation: 33
I'm building a login form in Next.js 14 (app router) with an Express.js backend (using Prisma for the database). I initially used a server action with a simple <form action={login} ...>
for submission, but I wanted to add client-side validation to provide instant feedback to users, allowing them to quickly see and correct input errors (like missing or invalid fields) without waiting for a server response.
To do this, I implemented Zod for validation and React-hook-form for form handling.
I'm confused about my error-handling strategy, especially the flow between the client and server.
My main questions are:
- Am I handling errors efficiently, or are there unnecessary steps in my implementation?
- Is using setError in react-hook-form the best way to map errors from the server?
- Should I rethink how the client and server handle validation ?
- Are there more efficient practices I should follow for server-side validation and error handling with Express.js and Prisma?
My component is now fully client-side. Here is the relevant code:
Front-end (Next.js) login form:
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { loginSchema, type LoginFormData } from '../../../../lib/types/loginSchema';
import { login } from '../../_actions/login';
const FormLogin = () => {
const router = useRouter();
const { register, handleSubmit, formState: { errors, isSubmitting }, setError, reset } = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
const result = await login(data);
if (result.errors) {
Object.entries(result.errors).forEach(([key, value]) => {
const errorMessage = Array.isArray(value) ? value.join(', ') : value;
setError(key as keyof LoginFormData, { type: 'manual', message: errorMessage });
});
} else if (result.success) {
router.push('/');
reset();
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 flex flex-col max-w-sm">
<input {...register('username')} className="border p-2" type="text" placeholder="username" />
{errors.username && <p className="text-red-500">{errors.username.message}</p>}
<input {...register('password')} className="border p-2" type="password" placeholder="password" />
{errors.password && <p className="text-red-500">{errors.password.message}</p>}
<button disabled={isSubmitting} type="submit" className="bg-blue-500 text-white disabled:bg-gray-500 py-2 rounded">
{isSubmitting ? 'Logging in...' : 'Login'}
</button>
{errors.root && <p className="text-red-500">{errors.root.message}</p>}
</form>
);
};
export default FormLogin;
Login action (server):
'use server';
import { loginSchema, type LoginFormData } from '../../../lib/types/loginSchema';
export async function login(data: LoginFormData) {
const result = loginSchema.safeParse(data);
if (!result.success) {
const { fieldErrors, formErrors } = result.error.flatten();
return { errors: { ...fieldErrors, root: formErrors.join(', ') } };
}
const { username, password } = result.data;
try {
const response = await fetch(`${process.env.EXPRESS_API_URL}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
credentials: 'include',
});
if (!response.ok) {
const errorResponse = await response.json();
const fieldErrors = {};
if (errorResponse.error.includes('username')) {
fieldErrors.username = errorResponse.error;
} else if (errorResponse.error.includes('password')) {
fieldErrors.password = errorResponse.error;
} else {
fieldErrors.root = errorResponse.error;
}
return { errors: fieldErrors };
}
return { success: true };
} catch (error) {
return { errors: { root: 'An unknown error occurred' } };
}
}
Express.js backend:
export const login = async (req, res) => {
try {
const { username, password } = req.body;
const user = await prisma.user.findUnique({ where: { username } });
if (!user) {
return res.status(400).json({ error: 'Invalid username' });
}
const isPasswordCorrect = await bcrypt.compare(password, user.password);
if (!isPasswordCorrect) {
return res.status(400).json({ error: 'Invalid password' });
}
generateToken(user.id, res);
res.status(200).json({ id: user.id, fullName: user.fullName, username: user.username });
} catch (error) {
res.status(500).json({ error: 'Internal Server Error' });
}
};
Zod Schema & type :
import { z } from 'zod';
export const loginSchema = z.object({
username: z.string().min(1, 'Username is required'),
password: z.string().min(1, 'Password is required'),
});
export type LoginFormData = z.infer<typeof loginSchema>;
Upvotes: 0
Views: 86