Search code examples
reactjsnode.jstypescriptmongodbexpress

Handling Form Data and User Registration in MERN Stack: Issues with FormData and Validation


I am working on a MERN stack application and I'm having trouble registering a new user. I have a form on the frontend that collects user data and sends it to the backend. The form data includes fields like username, password, confirmPassword, salary, roles, and email.

The problem is that my form data is not being processed correctly on the backend.

It reaches the corrects endpoints yet when I log the request all of the checked values are set as undefined even when the api-client of the frontend does correctly receives them.

I am using express-validator to check the relevant field. I'm not sure if I have to check every single field regardless if is not required by the User model.

These are the relevant endpoints as well as the User Model

//frontend apiClient

export const createUser = async(userRegisterData : FormData)=>{
    console.log(userRegisterData);
    const response = await fetch(${API_BASE_URI}/api/user/,{
        method:"POST",
        credentials: "include",
        headers:{
            "Content-Type": "application/json"
        },
        body: JSON.stringify(userRegisterData),
    });
    if (!response.ok) {
        throw new Error("Failed to register new user");
    }
    const data = await response.json();
    return data;
}

//Example of logged information

FormData(6) { username → "jussim", password → "123456", confirmPassword → "123456", salary → "2550", email → "[email protected]", "roles[0]" → "admin" }

//BACKEND endpoints // Routing and Express validator

router.post(
  "/",
  [
    check("username", "Username is required").isString(),
    check('password',"Password must be at least 6 characters").isLength({min: 6}),
     check("roles", "Roles are required").isArray(),
     check("email","Email is required").isEmail(),
  ],verifyToken,
  userRegister
);

//verifyToken Middleware

export const verifyToken = async(req: Request, res: Response, next: NextFunction)=>{
    const token = req.cookies["auth_cookie"];
    if(!token){
        return res.status(401).json({message:"Unauthorized"});
    }
    try {
        const decoded = jwt.verify(token,process.env.JWT_SECRET_KEY as string);
        req.userInfo = (decoded as JwtPayload).userInfo;
        next();       
    } catch (error) {
        console.log(error);
        res.status(401).json({message: "Unauthorized"});
    }
}

//userRegister controller

export const userRegister = async (req: Request, res: Response) => {
  const errors = validationResult(req);
  console.log(errors.array());
  if (!errors.isEmpty()) {
    return res.status(400).json({ message: errors.array() });
  }
  try {
    const { username, password, roles, email } = req.body;
    const isMatch = await User.findOne({
      username: username,
    });
    if (isMatch) {
      return res.status(400).json({ message: "User already exists" });
    }
    const hashedPassword = await bcrypt.hash(password, 10);

    const newUser = new User({
      username,
      password: hashedPassword,
      roles,
      email,
      createdBy: req.userInfo.userId,
    });
    await newUser.save();

    res.status(200).json({ message: "User registered succesfully!" });
  } catch (error) {
    console.log(error);
    res.status(500).json({ message: "Internal Server Error 500" });
  }
};

//User Model

export interface UserType {
  username: string;
  password: string;
  salary: number;
  roles: string[];
  email: string;
  isActive: boolean;
  createdBy: string;
}

const userSchema = new mongoose.Schema(
  {
    username: { type: String, required: true, unique: true },
    password: { type: String, required: true, minlength: 6 },
    salary: {type: Number, required: true, default: 2550},
    roles: { type: [String], required: true, default: ["client"] },
    email: { type: String, required: true, unique: true },
    isActive: { type: Boolean, required: true, default: false },
    createdBy: { type: String, required: true },
  },
  {
    timestamps: true,
  }
);

const User = mongoose.model<UserType & mongoose.Document>("user", userSchema);

As I was writting this post I noticed some errors, like the missing salary field on my controller. Yet I don't know if its the cause of none of the information reacher the controller.

When I logged the errors.array() all of the values come as undefined. Which makes me believe that that It can't be "read" (?. I honestly have no clue what I have to do. Or how can I make it so the information its at least usable from the backend.

In case is necessary I was practicing to use useFormContext so My form looks like this:

interface Props{
    handleRegister: (formData: FormData)=> void;
    isLoading: boolean;
}

export interface UserRegisterFormData {
    username: string;
    password: string;
    confirmPassword: string;
    salary: number;
    roles: string[];
    email: string;
}

const ManageUserRegisterForm = ({handleRegister,isLoading}:Props) => {
    
    const formMethods = useForm<UserRegisterFormData>()

    const {handleSubmit} = formMethods;

    const onSubmit = handleSubmit((formDataJson:UserRegisterFormData)=>{
        const formData = new FormData();
        formData.append('username',formDataJson.username);
        formData.append('password',formDataJson.password);
        formData.append('confirmPassword',formDataJson.confirmPassword);
        formData.append('salary', formDataJson.salary.toString());
        formData.append('email',formDataJson.email);
        formDataJson.roles.forEach((role,index)=>{
            formData.append(`roles[${index}]`, role);
        })

        handleRegister(formData);
        

    })




  return (
    <FormProvider {...formMethods}>
        <form className="flex flex-col gap-10" onSubmit={onSubmit}>
            <FirstPart/>
            <SecondPart/>
            <span>
            <button type="submit" disabled={isLoading} className="bg-blue-600 text-white p-2 font-bold hover:bg-blue-500 text-xl disabled:bg-gray-500">
            {isLoading ? "Loading..." : "Submit"}
            </button>
        </span>
        </form>
    </FormProvider>
  )
}

I was just messing around with it. I haven't tried to do it like this before so I am not sure If it might be the reason as to why my frontend data isn't accesible by my backend.

I'm still learning alot about fullstack so any feedback or advice is really appreciated! Thank you for your time!


Solution

  • When posting form data here:

    body: JSON.stringify(userRegisterData)
    

    you're actually sending a plain object (because userRegisterDatd/form data is not a plain JS object, so stringifying it ignores key-value properties, and yields empty object {} see JSON.stringify > Descripton), and your backend recieves an empty object.

    So, you need to get form data values as a plain JS object with Object.fromEntries() instead, and then stringify it and send it as JSON:

    Try this:

    body: JSON.stringify(Object.fromEntries(userRegisterData))