Day 01/100 - I converted my workflow to codes, and tested and documented the route using postman
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:
asyncHandler
: for handling asynchronous operations in the application.validateRegister
andvalidateLinkParent
: validating data, related to user registration and linking a parent to a student.ErrorResponse
: a custom utility for generating error responses with specific messages and status codes.generateRandomId
: to generate random studentID.Models: The code references several database models including
SuperAdminModel
,studentModel
,parentModel
, andclassArmModel
.createParent
andlinkParent
: 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 theclassArmModel
.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 theobject
.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 };