Skip to content
Santekno.com | Tech Tutorials and Trends
EN
📖 0%
30 Sep 2025 · 5 min read ·Article 92 / 125
Go

92 Case Study: A Todo App with Auth and DB

IH
Ihsan Arif
Writer at Santekno · Backend Engineer

92 Case Study: A Todo App with Auth and DB

In the world of software development, building a simple to-do app might sound basic. But once we add in authentication (auth) and database (DB) integration, the complexity becomes very real—and in real-world production, these “small” things are often the foundation of a software engineer’s core skills.

In this article, I want to share the “92nd” case study from my experience mentoring teams at a bootcamp. We’ll discuss how to build a todo app with auth and DB—with practical examples, diagrams, and simulations of the edge cases that often get overlooked. For this case study, I’ll use a stack of Express.js, MongoDB, and JWT.


1. Architecture Overview

Let’s take a high-level look at the flow of this application.

Mermaid Flow Diagram

MERMAID
flowchart LR
    A[User] -->|Register/Login| B[API Auth Route]
    B -->|JWT Token| A
    A -->|Token| C[API Todo Route]
    C -->|CRUD Operation| D[Database (Todos Collection)]

In broad strokes:

  • The user registers/logs in, and the server returns a JWT token.
  • That token is used to access the TODO routes.
  • The TODO routes perform CRUD operations against the database.
  • Each todo is tied to its owning user (auth).

2. Database Structure

The application requires two collections (MongoDB):

CollectionFields
users_id, username, password_hash
todos_id, user_id, title, description, is_done

For example, a todo document looks like:

json
1{
2  "_id": "664f3bf...",
3  "user_id": "664f3be...",
4  "title": "Beli Kopi",
5  "description": "Pastikan arabika!",
6  "is_done": false
7}


3. Core Code: Express + MongoDB + JWT

Let’s break the code down into three main parts.

a. Auth Setup

js
 1// authController.js
 2const bcrypt = require('bcrypt');
 3const jwt = require('jsonwebtoken');
 4const User = require('./models/User');
 5
 6// REGISTER
 7exports.register = async (req, res) => {
 8  const {username, password} = req.body;
 9  const hash = await bcrypt.hash(password, 8);
10  const user = await User.create({username, password_hash: hash});
11  res.status(201).json({message: 'User registered', user: user.username});
12};
13
14// LOGIN
15exports.login = async (req, res) => {
16  const {username, password} = req.body;
17  const user = await User.findOne({username});
18  if (!user) return res.status(401).json({message: 'User not found'});
19
20  const isMatch = await bcrypt.compare(password, user.password_hash);
21  if (!isMatch) return res.status(401).json({message: 'Invalid password'});
22
23  const token = jwt.sign({userId: user._id}, process.env.JWT_SECRET, {expiresIn: '1h'});
24  res.json({token});
25};

b. Auth Middleware

js
 1// middleware/auth.js
 2const jwt = require('jsonwebtoken');
 3
 4module.exports = (req, res, next) => {
 5  const token = req.headers['authorization']?.split(' ')[1];
 6  if (!token) return res.status(401).json({message: 'Token missing'});
 7
 8  try {
 9    const decoded = jwt.verify(token, process.env.JWT_SECRET);
10    req.user = decoded;
11    next();
12  } catch (err) {
13    res.status(401).json({message: 'Invalid/expired token'});
14  }
15};

c. Todo CRUD

js
 1// todoController.js
 2const Todo = require('./models/Todo');
 3
 4// GET ALL TODOs (per user)
 5exports.getTodos = async (req, res) => {
 6  const todos = await Todo.find({user_id: req.user.userId});
 7  res.json(todos);
 8};
 9
10// CREATE
11exports.createTodo = async (req, res) => {
12  const todo = await Todo.create({
13    user_id: req.user.userId,
14    title: req.body.title,
15    description: req.body.description,
16    is_done: false,
17  });
18  res.status(201).json(todo);
19};
20
21// UPDATE
22exports.updateTodo = async (req, res) => {
23  const todo = await Todo.findOneAndUpdate(
24    {_id: req.params.id, user_id: req.user.userId},
25    req.body,
26    {new: true}
27  );
28  if (!todo) return res.status(404).json({message: 'Todo not found'});
29  res.json(todo);
30};
31
32// DELETE
33exports.deleteTodo = async (req, res) => {
34  const result = await Todo.deleteOne({_id: req.params.id, user_id: req.user.userId});
35  if (result.deletedCount === 0) return res.status(404).json({message: 'Todo not found'});
36  res.json({message: 'Deleted'});
37};

4. Simulation: Request Flow & Edge Cases

Let’s simulate some edge cases that frequently come up:

a. A user tries to access another user’s data

The client sends the request:

text
1DELETE /api/todo/62e2d3...
2Authorization: Bearer xyz...

The endpoint looks up the todo by both _id AND user_id:

js
1Todo.deleteOne({_id: req.params.id, user_id: req.user.userId});
What if the todo belongs to another user? It’s not found. Response:
json
1{
2  "message": "Todo not found"
3}
Best practice: Avoid returning 403; just return 404 to prevent information leakage.

b. Expired Token

If the JWT has expired, every auth-protected access is immediately rejected.

json
1{
2  "message": "Invalid/expired token"
3}


5. Testing Scenarios in Postman

ScenarioEndpointResult
Register UserPOST /api/auth/register201, plus the username
Login UserPOST /api/auth/loginJWT token in the JSON
Get Todos (without auth)GET /api/todo401 Unauthorized
Get Todos (with auth)GET /api/todoList of todos belonging to the token’s user
Update TodoPUT /api/todo/:id200 OK if owned, 404 if not owned by the user
Delete another user’s TodoDELETE /api/todo/:id404 Not Found

6. Optional: Frontend Integration

The frontend can use React. Store the token in localStorage or a cookie (HttpOnly if you’re concerned about XSS). Teach users to handle expired tokens (auto-logout/refresh).


7. Production Practices

A few tips when deploying to production:

  1. Encrypt the JWT Secret properly (never hardcode it).
  2. Never log user passwords.
  3. Limit the request rate using express-rate-limit to prevent login brute-force.
  4. Sanitize input to prevent injection (MongoDB needs special attention here too).
  5. Monitor errors via log tailing tools like Sentry.

8. Performance Case Study (Bonus Table)

Number of UsersAvg. Register TimeTodo Access TimeCollection Size
1000120ms25ms140KB
10000130ms28ms1.2MB
100000135ms33ms12MB

(Tested on a free-tier Mongo Atlas)


9. Conclusion & Improvements

Building a to-do app with authentication and a database looks simple on paper, but there are so many production-grade aspects to pay attention to: relaying the JWT, designing the DB schema, access control, all the way to handling edge cases. This 92nd case study proves it: “simplicity is deceptive—implementation is where the devil hides.”

Possible improvements:

  • Refresh token flow.
  • Soft delete (archive instead of delete).
  • Email verification/password reset.
  • GDPR-compliant logging.

If you’d like to discuss startup-scale authentication patterns or best practices for deploying to the cloud further, feel free to comment—hands-on field experience is the best teacher, and we learn from real case studies, not just theory.


**Happy debugging! 🚀**

Related Articles

💬 Comments