Day 01/100 - I converted my workflow to codes, and tested and documented the route using postman

Day 01/100 - I converted my workflow to codes, and tested and documented the route using postman

Β·

12 min read

I start #100daysofcoding in the name of the Almighty 🀲.

There's this application I and my colleagues have been building for a start-up. And starting from today, I have decided to track my improvement, and make myself accountable using this medium - writing blog and posting on social media my daily code related activities, because I believe it will help me with consistency and I will learn more from others' opinions, as we don't have senior developers on the team πŸ₯Ή.

This app is an SMS - School Management System, and I am precisely working on the backend (Express and MongoDb).

So, today being the first day, I worked on transferring some API workflows drafted by me to codes - represented my thought process visually.

I drafted this to reduce extensive thinking while writing codes. And it can also serve as a guide for anyone that's coming to update the code.

The above is a workflow for registering students and at the same time linking them to their guardians/parents.

I created the route file ./route/student.js

const express = require("express");

const multipleProtect = require("../middleware/multipleAuth");
const router = express.Router();

const { registerStudent, linkNewParent } = require("../controller/student");

// register student
router.post("/register", multipleProtect(["super admin", "admin"]), registerStudent);

module.exports = router;

I created a middleware. The multipleProtect middleware is imported to handle authentication for the routes.

The POST route at "/register" is used to register a student. It is protected by the multipleProtect middleware to ensure only "super admin" (the school itself) or "admin" users can access it, as drawn in the diagram 😊.

Now, let's move to the real function - registerStudent.js

const { asyncHandler } = require("../middleware");
const { validateRegister, validateLinkParent } = require("../middleware/validation");
const ErrorResponse = require("../utils/errorResponse");
const { generateRandomId } = require("../utils/generateRandomId");
const {
  SuperAdminModel,
  studentModel,
  parentModel,
  classArmModel,
} = require("../models");
const { createParent } = require("../utils/createParent");
const { linkParent } = require("../utils/linkParent");

Here we have:

  1. asyncHandler: for handling asynchronous operations in the application.

  2. validateRegister and validateLinkParent: validating data, related to user registration and linking a parent to a student.

  3. ErrorResponse: a custom utility for generating error responses with specific messages and status codes.

  4. generateRandomId: to generate random studentID.

  5. Models: The code references several database models including SuperAdminModel, studentModel, parentModel, and classArmModel.

  6. createParent and linkParent: used for creating and linking parent entities to the student entities in the application.

    continuation πŸ‘‡

     exports.registerStudent = asyncHandler(async (req, res, next) => {
       try {
          // code for student registration goes
       } catch (error) {
         console.error(`error from registering student: ${error}`);
         return next(
           new ErrorResponse(
             "error registering student, please try again later",
             500
           )
         );
       }
     });
    

As seen above, asyncHandler is used to wrap the asynchronous function, which takes in three parameters - req (request), res (response), and next (next middleware). try and catch is used for error handling.

writing the codes for registration starts here

Validating the request body:

const { error } = validateRegister(req.body);
if (error) {
  return next(new ErrorResponse(error.details[0].message, 400));
}

I used joi for validating the request body, you can find the code in validateRegister below the article. If there are validation errors, an ErrorResponse is created with a 400 status code and the first error message from the validation result. The error response is passed to the next middleware using the next function.

School ID Extraction

From the middleware function - multipleProtect (you can find the code in it below the article) used in ./route/student, I stored the authorized users information in req.user object, in which I extracted the schoolId in the below code πŸ‘‡.

const schoolId = req.user.schoolName ? req.user.id : req.user.schoolId;

If the user accessing the route has a prop (schoolName) which is the school, extract the id, and if otherwise - admin, extract the value of the prop (schoolId). I actually did this to make the database query more specific, avoiding other schools accessing another school's students' info.

Destructuring of request body

    const {
      classArmId,
      gender,
      surName,
      email,
      dateOfBirth,
      studentID,
      firstName,
      otherName,
      phoneNumber,
      country,
      stateOfOrigin,
      localGovernmentArea,
      address,
      parentID,
      relationship,
      parentTitle,
      parentFirstName,
      parentSurName,
      parentGender,
      maritalStatus,
      parentEmail,
      parentPhoneNumber,
      parentCountry,
      occupation,
      parentAddress,
    } = req.body;

ClassArm Validation:

const classArm = await classArmModel.findOne({
  _id: classArmId,
  schoolId,
});
if (!classArm) {
  return next(new ErrorResponse(`classArm with id: ${classArmId} doesn't exist in your school`, 404));
}
  • Checks if the specified classArmId exists in the school using the classArmModel.

  • If not found, returns a 404 error response indicating that the classArm doesn't exist in the school.

Generating Student ID:

const studentId = studentID ? studentID : await generateRandomId(schoolId, SuperAdminModel, studentModel);

Determines the studentId. If provided in the request (studentID), it uses that; otherwise, it generates a random ID using the generateRandomId function (you can check the code in it below the article). Though, I can explain what the function does briefly. It generates a new studentID, checks if it exists, and if it does, it generates another. It repeats until it is confirmed that it's unique.

Checking Existing Student:

It is important to check if the student with the studentId already exist in the school.

const student = await studentModel.findOne({
  schoolId,
  studentID: studentId,
});
if (student) {
  return next(new ErrorResponse(`student with id: ${studentId} already exists in your school`, 400));
}
  • Checks if a student with the same studentId already exists in the school.

  • If found, returns a 400 error response indicating that a student with the specified ID already exists.

Creating Student Object:

    let object = {
      studentID: studentId,
      firstName,
      surName,
      otherName: otherName || null,
      email: email || null,
      classArmId,
      gender,
      dateOfBirth,
      phoneNumber: phoneNumber || null,
      country,
      stateOfOrigin,
      localGovernmentArea,
      address,
      password: studentId,
      schoolId,
      guardians: []
    };

Parent ID Handling:

Another thing is when creating student, you can link it with an existing parent using the parent's id or you can create a new parent/guardian for it πŸ‘‡

    let parentId;

// if there's no parentID from the request body,
// we are creating a new parent
    if (!parentID || parentID.trim() === "") {
// in creating a new parent, these fields (parentTitle, parentFirstName,...) are very important
      const requiredFields = [
        { field: "parent title", value: parentTitle },
        { field: "parent first name", value: parentFirstName },
        { field: "parent surname", value: parentSurName },
        { field: "parent gender", value: parentGender },
        { field: "parent marital status", value: maritalStatus },
        { field: "parent email", value: parentEmail },
        { field: "parent phone number", value: parentPhoneNumber },
        { field: "parent country", value: parentCountry },
        { field: "parent occupation", value: occupation },
        { field: "parent address", value: parentAddress },
      ];

      for (const fieldInfo of requiredFields) {
        const { field, value } = fieldInfo;
        if (!value) {
          return next(
            new ErrorResponse(`${field} is needed to create a new parent`, 400)
          );
        }
      }

// parent's data is piled up here
      const data = {
        title: parentTitle,
        surName: parentSurName,
        firstName: parentFirstName,
        maritalStatus,
        email: parentEmail,
        phoneNumber: parentPhoneNumber,
        occupation,
        address: parentAddress,
        gender: parentGender,
        country: parentCountry,
        schoolId,
      };

// createParent function checks if the parent already exists by email and schoolId
// if yes, returns null. if no, creates new parent and return its info.
      const parent = await createParent(
        parentEmail,
        schoolId,
        parentModel,
        data,
      );

// if it returns null, we now know the reason πŸ€ͺ
      if (!parent) {
        return next(
          new ErrorResponse(
            `parent with email address: ${parentEmail} already exists in the school. therefore you can't create a new one.`
          )
        );
      }

// the id of the parent is needed
      parentId = parent.id;
// so it can be added as a guardian for the new student
// the student object is updated - a guardian πŸ₯³
      object.guardians.push({
        parentId,
        relationship
      })
    } else {
// what if the parentID was provided in the request body? πŸ€”
      parentId = parentID;
// all needed is to link then
// linkParent function checks if the parent exists,
// if parent doesn't exist it returns null
// otherwise it update the student object - guardian, and returns it.
      object = await linkParent(
        parentId,
        schoolId,
        relationship,
        object
      );
 // now we know the reason for this πŸ€ͺ
      if (!object) {
        return next(
          new ErrorResponse(
            "Parent doesn't exist. Therefore you can't link it to the student"
          )
        );
      }
    }

I've tried commenting the code for brief explanation πŸ™

Saving New Student:

the updated object is finally used here πŸ‘‡

const newStudent = new studentModel(object);
await newStudent.save();
  • Creates a new instance of the studentModel with the details in the object.

  • Saves the new student to the database.

Response:

Time for response. very very important. I heard adding the success property in response object is unnecessary because the status code is there to tell us the outcome.

The thing is... this is the format we've been using from the beginning, and I can change it once we're done with the task. Any opinions please πŸ™. DM on twitter

res.status(201).json({
  success: true,
  message: `student created and linked successfully`,
  data: {
    student: newStudent,
    parentId,
  },
});
  • Responds with a 201 status code and a JSON object indicating the success of the operation.

  • Includes information about the newly created student and the associated parent ID.

Testing with Postman:

It's really important to test the route before pushing to remote location. Though, I shall still engage in raw testing with Jest.

Here I created new parent alongside the student, but the generated studentIDπŸ€¦β€β™‚οΈis undefined6639. That's because the function for generating the ID uses the school initials as prefix. While working on school registration, I didn't make the school initials required. I will make sure I fix that.

Here, I linked an existing parent to the student.

Documentation with Postman:

It is very important as it will serve as guide for frontenders on how the API can be consumed effectively 😊.

Thanks for reading the article πŸ™‡. Please if you notice anything unusual or not good, reach out. I'm open to corrections as they will only make me better.

I need to know if I'm on the right track or I'm doing it wrongly. God bless πŸ™.

Below are the codes for some of the utility functions and middlewares; πŸ‘‡

validateRegister for validating the request body.

const Joi = require("joi");

const schema = Joi.object({
    classArmId: Joi.string().required(),
    gender: Joi.string().valid("male", "female").required(),
    surName: Joi.string().required(),
    email: Joi.string().email(),
    dateOfBirth: Joi.date().required(),
    studentID: Joi.string(),
    firstName: Joi.string().required(),
    otherName: Joi.string(),
    phoneNumber: Joi.string(),
    country: Joi.string().required(),
    stateOfOrigin: Joi.string().required(),
    localGovernmentArea: Joi.string().required(),
    address: Joi.object({
        number: Joi.number(),
        street: Joi.string(),
        city: Joi.string(),
        state: Joi.string(),
        postalCode: Joi.string(),
        country: Joi.string()
    }).required(),
    parentID: Joi.string(),
    relationship: Joi.string().required(),
    parentTitle: Joi.string(),
    parentFirstName: Joi.string(),
    parentSurName: Joi.string(),
    parentGender: Joi.string(),
    maritalStatus: Joi.string(),
    parentEmail: Joi.string().email(),
    parentPhoneNumber: Joi.string(),
    parentCountry: Joi.string(),
    occupation: Joi.string(),
    parentAddress: Joi.object({
        number: Joi.number(),
        street: Joi.string(),
        city: Joi.string(),
        state: Joi.string(),
        postalCode: Joi.string(),
        country: Joi.string()
    })
});

const validate = (data) => {
    return schema.validate(data, { abortEarly: false });
};

module.exports = validate;

generateRandomId

const generateRandomId = async (schoolId, schoolModel, studentModel) => {
  const school = await schoolModel.findById(schoolId);

  // extract the school abbreviation
  const abbr = school.schoolInitials;

  // generate a random number
  const randomNumber = Math.floor(1000 + Math.random() * 9000);

  // Combine school abbreviation and random number to create the student ID
  const studentId = `${abbr}${randomNumber}`;

  // check if studentID exists in the school, and if so, re-create
  await check(studentModel, studentId, schoolId);

  return studentId;
};

const check = async(model, studentId, schoolId) => {
  const exist = await model.findOne({
    studentID: studentId,
    schoolId
  });

  if (exist) {
    await check(model, studentId, schoolId);
  }
};

module.exports = {
  generateRandomId
}

multipleProtect

const jwt = require("jsonwebtoken");
const ErrorResponse = require("../utils/errorResponse");
const {
  SuperAdminModel,
  staffModel,
  studentModel,
  parentModel,
} = require("../models");

const protect = (allowedRoles) => async (req, res, next) => {
  const token = req.signedCookies["token"];

  if (!token || token.trim == "") {
    return next(new ErrorResponse('Unauthorized!. Please sign up or log in', 401));
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);

    let user = null;

    // Check if any of the allowed roles match any of the user's roles
    user = await staffModel.findOne({
      _id: decoded.id,
      role: { $in: allowedRoles },
    });

    // Check if the user is a SuperAdmin and has the "super admin" role
    if (!user && allowedRoles.includes("super admin")) {
      user = await SuperAdminModel.findOne({ _id: decoded.id });
    }

    // Check if the user is a Student and has the "student" role
    if (!user && allowedRoles.includes("student")) {
      user = await studentModel.findOne({ _id: decoded.id });
    }

    // Check if the user is a Parent and has the "parent" role
    if (!user && allowedRoles.includes("parent")) {
      user = await parentModel.findOne({ _id: decoded.id });
    }

    if (!user) {
      return next(
        new ErrorResponse(
          "Not authorized to access this route! Please try logging in again",
          401
        )
      );
    }
// store user info
    req.user = user;
    next();
  } catch (err) {
    return next(new ErrorResponse("Not authorized to access this route", 401));
  }
};

module.exports = protect;

studentModel

const mongoose = require('mongoose');

const addressSchema = new mongoose.Schema({
    number: Number,
    street: String,
    city: String,
    state: String,
    postalCode: String,
    country: String
});

const studentSchema = new mongoose.Schema({
    studentID: {
        type: String,
        required: true,
        unique: true
    },
    firstName: {
        type: String,
        required: true
    },
    surName: {
        type: String,
        required: true
    },
    otherName: {
        type: String,
    },
    email: {
        type: String,
    },
    classArmId: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'ClassArm',
        required: true
    },
    gender: {
        type: String,
        enum: ['male', 'female', 'other'],
        required: true
    },
    dateOfBirth: {
        type: Date,
        required: true
    },
    phoneNumber: {
        type: String
    },
    country: {
        type: String,
        required: true
    },
    photo: String, // You can store the photo URL or file path here
    guardians: [
        {
            parentId: {type: mongoose.Schema.Types.ObjectId, ref: "Parent"},
            relationship: {
                type: String
            }
        }
    ],
    bloodGroup: String,
    stateOfOrigin: {
        type: String,
        required: true
    },
    localGovernmentArea: {
        type: String,
        required: true
    },
    address: {
        type: addressSchema,
        required: true
    },
    password: {
        type: String,
        required: true
    },
    status: {
        type: "String",
        enum: ["active", "deactivated"],
        required: true,
        default: "active"
    },
    schoolId: {
        type: mongoose.Schema.Types.ObjectId,
        ref: "SuperAdmin"
    },
    resetPasswordToken: String,
    resetPasswordExpire: Date,
}, {timestamps: true});

const Student = mongoose.model('Student', studentSchema);

module.exports = Student;

parentModel

const mongoose = require('mongoose');

const addressSchema = new mongoose.Schema({
    number: Number,
    street: String,
    city: String,
    state: String,
    postalCode: String,
    country: String
});

const parentSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    surName: {
        type: String,
        required: true
    },
    firstName: {
        type: String,
        required: true
    },
    maritalStatus: {
      type: String,
      enum: ["single", "married"],
        required: true
    },
    email: {
        type: String,
        required: true,
        unique: true,
        // Add email validation using a regular expression (regex)
        validate: {
            validator: function(value) {
                // Regular expression for a valid email address
                const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
                return emailRegex.test(value);
            },
            message: 'Invalid email address'
        }
    },
    phoneNumber: {
        type: String,
        required: true
    },
    emergencyContact: {
        name: String,
        phoneNumber: String
    },
    occupation: {
        type: String,
        required: true
    },
    address: {
        type: addressSchema
    },
    gender: {
        type: String,
        enum: ["male", "female"],
        required: true
    },
    country: {
        type: String,
        required: true
    },
    schoolId: {
        type: mongoose.Schema.Types.ObjectId,
        ref: "SuperAdmin"
    },
    resetPasswordToken: String,
      resetPasswordExpire: Date,
}, { timestamps: true });

const Parent = mongoose.model('Parent', parentSchema);

module.exports = Parent;

createParent

const createParent = async (email, schoolId, model, data) => {
    // check if the parent exists in the school by email address
    const parent = await model.findOne({
        email,
        schoolId
    });

    if (parent) {
        return null;
    }

    // create a new parent using the tabled info
    const newParent = new model(data);
    newParent.save();

    return newParent;
};

module.exports = { createParent }

linkParent

const { parentModel } = require("../models")

const linkParent = async (parentId, schoolId, relationship, object) => {
// does parent exist in the school?
    const parent = await parentModel.findOne({
        _id: parentId,
        schoolId
    });
// no, return null
    if (!parent) {
        return null;
    }
// otherwise, update student object
    const guardian = {
        parentId,
        relationship
    }

    object.guardians.push(guardian);

    return object;
};

module.exports = { linkParent };
Β