mdnc
mdnc

Reputation: 33

Best Practices for Handling Errors in Next.js 14 Login Form with Express.js Backend, server action and client side

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

Answers (0)

Related Questions