This post documents how I implemented user registration, authentication, and database persistence using a TypeScript-based Nodejs Express backend with MongoDB, JWT, and bcrypt.
Click here for Act 5 · Scene 2
Table of Contents
- Overview
- User Model
- Database Choice and Setup
- Registering the First User
- Login and Logout
- Validation with Postman
- Mental Model
- Why this scene matters
Act 6 · Scene 1: Persisting the First User (Models, Database, Auth Flow)
Overview
Until now, the backend has only proven that it could start, route requests, and respond correctly. In this scene, it moves into something real. I introduce a user model, connect the server to a database, and implement the full authentication flow. That is: register, login, and logout. I verify everything end to end before touching the frontend again.
At this point, the server was stable, middleware was in place, and routes were wired correctly. The next logical step was to introduce persistence and authentication logic.
This scene focuses on connecting the backend to a database, defining a user model, and implementing the first complete authentication flow: registration, login, and logout validated entirely through Postman before shifting back to the frontend.
Let’s jump right in…
User Model
I started by defining a User model using Mongoose. The model establishes the shape, constraints, and validation rules for user data at the database level.
const UserSchema = new mongoose.Schema(
{
name: {
type: String,
trim: true,
required: [true, "please enter name"],
minlength: [3, "name shouldn't be less than 3 letters"],
maxlength: [20, "name shouldn't be maore than 20 letters"],
},
email: {
type: String,
trim: true,
required: [true, "please enter email"],
unique: true,
match: [
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
"Please provide a valid email",
],
},
password: {
type: String,
trim: true,
required: [true, "please enter password"],
minlength: [6, "password shouldn't be less than 6 letters"],
},
isRegistered: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);
export default mongoose.model("User", UserSchema);
Database Choice and Setup
I considered SQL. But I chose MongoDB.
To me, flexibility matters more than rigid schemas. Authentication data evolves. MongoDB makes that painless without sacrificing structure.
At this stage, the database runs locally using MongoDB Compass. The server connects during startup, ensuring persistence is available before accepting requests.
Please note that the connection string will later be moved to environment variables for production readiness.
Here is the code for the connection:
await mongoose.connect("mongodb://localhost:27017/CV-ANALYZER");
console.log("connected to DB")
app.listen(PORT,"localhost",()=>{
console.log(`server listening on port ${PORT}`)
})
The index.ts file was where I placed it.
Registering the First User
The registration controller validates input:
if (!name || !email || !password) {
console.log("input fields shouldn't be empty");
return res.status(400).json({ msg: "input fields shouldn't be empty" });
}
Checks for existing users:
const emailExists = await User.findOne({ email: email });
if (emailExists) {
console.log("kindly log in");
return res.status(401).json({ msg: "kidly log in" });
}
Hashes passwords using bcrypt
const salt = await bcrypt.genSalt(12);
const hashedPassword = await bcrypt.hash(password, salt);
Only when everything checks out does the user get written to the database.
And right there, that’s the line between “toy backend” and “real system.”
And I must state that no tokens are issued here. Registration and authentication are treated separately.
Login and Logout
The login controller verifies credentials:
if (!email || !password) {
console.log("input fields shouldn't be empty");
return res.status(400).json({ msg: "input fields shouldn't be empty" });
}
Validates password hashes:
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
console.log("invalid credentials");
return res.status(400).json({ msg: "invalid credentials" });
}
Issues a signed JWT:
const token = jwt.sign(
{ userId: user._id, userName: user.name },
process.env.JWT_SECRET_V!,
{ expiresIn: "1d" }
);
And stored as an HTTP-only cookie:
res.cookie("token", token, {
path: "/",
httpOnly: true,
expires: new Date(Date.now() + oneDay),
// secure:true,
// sameSite:"none"
});
Logout is handled by clearing the authentication cookie, immediately invalidating the session.
res.cookie("token", "", {
path: "/",
httpOnly: true,
expires: new Date(0),
// secure:true,
// sameSite:"none"
});
Validation with Postman
All authentication routes were tested using Postman:
I registered a user with this:
{
"name":"voke",
"email": "voke@gmail.com",
"password": "xxxxxxx"
}
Postman output for Register:
I logged in as a User:
{
//"name":"voke",
"email": "voke@gmail.com",
"password": "xxxxxxx"
}
Postman output for Login:
I then logged out the User:
Postman output for logout:
With all these done, I think it's safe for me to pay a visit to the frontend of this wonderful application.
Mental Model
Right now:
- Authentication data persists correctly
- Passwords are securely hashed
- Tokens are issued and removed cleanly
- The backend no longer depends on UI
Why This Scene Matters
This scene shows that I:
- Implement authentication with security first practices
- Validate backend behavior independently
- Build systems ready for real users
Thanks for reading.
Let’s move on to the Next Scene.
We in the Building…
Building in Progress…



Top comments (0)