Node.js  

TODO app with CRUD endpoints using Node.js (Express), Vue 3 (Vite), and MongoDB

Overview

  • Backend: Node.js + Express + Mongoose (MongoDB)

  • Frontend: Vue 3 + Vite

  • Database: MongoDB (local or Atlas Cloud)

โš™๏ธ Step 1. Install MongoDB

You have two options:

Option A: Local MongoDB

  1. Download MongoDB Community Server

  2. Install it, start the MongoDB service, and verify:

mongo --version
  1. Default connection string:

mongodb://localhost:27017/todoapp

Option B: MongoDB Atlas (cloud)

mongodb+srv://<username>:<password>@cluster0.xxxxx.mongodb.net/todoapp

๐Ÿ—„๏ธ Step 2. Install Dependencies in Backend

Go to your backend folder (server/) and run:

npm install mongoose dotenv

๐Ÿ“ Step 3. Create .env file

Create a .env file in server/:

MONGO_URI=mongodb://localhost:27017/todoapp
PORT=3000

(If you use Atlas, replace the URI accordingly.)

๐Ÿงฉ Step 4. Update Backend Code (server/index.js)

Replace your server/index.js content with the following:

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

// --- Setup ---
const app = express();
app.use(cors());
app.use(express.json());

// --- MongoDB Connection ---
mongoose.connect(process.env.MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})
.then(() => console.log('โœ… MongoDB connected'))
.catch(err => console.error('โŒ MongoDB connection error:', err));

// --- Todo Schema & Model ---
const todoSchema = new mongoose.Schema({
  title: { type: String, required: true },
  completed: { type: Boolean, default: false }
}, { timestamps: true });

const Todo = mongoose.model('Todo', todoSchema);

// --- Routes ---
app.get('/api/todos', async (req, res) => {
  const todos = await Todo.find().sort({ createdAt: -1 });
  res.json(todos);
});

app.post('/api/todos', async (req, res) => {
  const { title } = req.body;
  if (!title?.trim()) return res.status(400).json({ error: 'Title required' });

  const todo = new Todo({ title: title.trim() });
  await todo.save();
  res.status(201).json(todo);
});

app.put('/api/todos/:id', async (req, res) => {
  const { id } = req.params;
  const { title, completed } = req.body;

  try {
    const todo = await Todo.findByIdAndUpdate(
      id,
      { title, completed },
      { new: true }
    );
    if (!todo) return res.status(404).json({ error: 'Todo not found' });
    res.json(todo);
  } catch (err) {
    res.status(400).json({ error: 'Invalid ID' });
  }
});

app.delete('/api/todos/:id', async (req, res) => {
  const { id } = req.params;
  try {
    const todo = await Todo.findByIdAndDelete(id);
    if (!todo) return res.status(404).json({ error: 'Todo not found' });
    res.json(todo);
  } catch (err) {
    res.status(400).json({ error: 'Invalid ID' });
  }
});

// --- Start Server ---
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`๐Ÿš€ Server running at http://localhost:${PORT}`));

๐Ÿ“ฆ Step 5. Folder structure

node-vue-todo/
โ”‚
โ”œโ”€โ”€ server/
โ”‚   โ”œโ”€โ”€ index.js
โ”‚   โ”œโ”€โ”€ package.json
โ”‚   โ”œโ”€โ”€ .env
โ”‚   โ””โ”€โ”€ node_modules/
โ”‚
โ””โ”€โ”€ client/
    โ”œโ”€โ”€ src/
    โ”œโ”€โ”€ package.json
    โ””โ”€โ”€ node_modules/

โ–ถ๏ธ Step 6. Run the app

Run backend

cd server
npm run dev

You should see

โœ… MongoDB connected
๐Ÿš€ Server running at http://localhost:3000

Run frontend

cd client
npm run dev

Go to http://localhost:5173
Now your Vue app reads/writes todos to MongoDB ๐ŸŽ‰

๐Ÿง  Step 7. Verify in Mongo Shell

To check inserted data:

# connect to Mongo shell
mongosh

# switch to database
use todoapp

# list todos
db.todos.find().pretty()

Now, adding these features :
โœ… Pagination (limit, skip)
โœ… Filtering (completed / pending)
โœ… User Authentication (JWT login & protected routes)

๐Ÿงฉ Step 1. Install extra dependencies

In your server/ folder:

npm install bcryptjs jsonwebtoken

๐Ÿงฑ Step 2. Project structure

Update your backend to have this structure:

server/
โ”œโ”€โ”€ models/
โ”‚   โ”œโ”€โ”€ user.js
โ”‚   โ””โ”€โ”€ todo.js
โ”œโ”€โ”€ routes/
โ”‚   โ”œโ”€โ”€ auth.js
โ”‚   โ””โ”€โ”€ todos.js
โ”œโ”€โ”€ middleware/
โ”‚   โ””โ”€โ”€ authMiddleware.js
โ”œโ”€โ”€ index.js
โ”œโ”€โ”€ .env
โ””โ”€โ”€ package.json

๐Ÿง  Step 3. .env file

Add these:

MONGO_URI=mongodb://localhost:27017/todoapp
JWT_SECRET=SuperSecretKeyHere123
PORT=3000

๐Ÿงฉ Step 4. Create

models/user.js

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: { type: String, required: true, unique: true },
  password: { type: String, required: true }
}, { timestamps: true });

// Hash password before save
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// Compare password
userSchema.methods.comparePassword = function(password) {
  return bcrypt.compare(password, this.password);
};

module.exports = mongoose.model('User', userSchema);

๐Ÿ—‚๏ธ Step 5. Create

models/todo.js

const mongoose = require('mongoose');

const todoSchema = new mongoose.Schema({
  title: { type: String, required: true },
  completed: { type: Boolean, default: false },
  userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
}, { timestamps: true });

module.exports = mongoose.model('Todo', todoSchema);

๐Ÿ”‘ Step 6. Auth Middleware (middleware/authMiddleware.js)

const jwt = require('jsonwebtoken');
require('dotenv').config();

function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer '))
    return res.status(401).json({ error: 'Unauthorized' });

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

module.exports = authMiddleware;

๐Ÿ” Step 7. Auth Routes (routes/auth.js)

const express = require('express');
const jwt = require('jsonwebtoken');
const User = require('../models/user');
require('dotenv').config();

const router = express.Router();

// Register
router.post('/register', async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password)
    return res.status(400).json({ error: 'Username and password required' });

  try {
    const existing = await User.findOne({ username });
    if (existing) return res.status(400).json({ error: 'Username already exists' });

    const user = new User({ username, password });
    await user.save();
    res.status(201).json({ message: 'User registered successfully' });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Login
router.post('/login', async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password)
    return res.status(400).json({ error: 'Username and password required' });

  try {
    const user = await User.findOne({ username });
    if (!user || !(await user.comparePassword(password)))
      return res.status(401).json({ error: 'Invalid credentials' });

    const token = jwt.sign(
      { userId: user._id, username: user.username },
      process.env.JWT_SECRET,
      { expiresIn: '1d' }
    );
    res.json({ token });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

module.exports = router;

๐Ÿ“ Step 8. Todo Routes (routes/todos.js)

Supports:

  • Pagination (limit, skip)

  • Filtering by completion (completed=true/false)

  • Auth-protected access (user-specific todos)

const express = require('express');
const Todo = require('../models/todo');
const auth = require('../middleware/authMiddleware');
const router = express.Router();

// GET todos with pagination & filter
router.get('/', auth, async (req, res) => {
  const { completed, limit = 10, skip = 0 } = req.query;
  const query = { userId: req.user.userId };

  if (completed !== undefined) {
    query.completed = completed === 'true';
  }

  const todos = await Todo.find(query)
    .sort({ createdAt: -1 })
    .skip(Number(skip))
    .limit(Number(limit));

  const total = await Todo.countDocuments(query);
  res.json({ total, todos });
});

// POST create todo
router.post('/', auth, async (req, res) => {
  const { title } = req.body;
  if (!title?.trim()) return res.status(400).json({ error: 'Title required' });

  const todo = new Todo({ title: title.trim(), userId: req.user.userId });
  await todo.save();
  res.status(201).json(todo);
});

// PUT update
router.put('/:id', auth, async (req, res) => {
  const { id } = req.params;
  const { title, completed } = req.body;

  const todo = await Todo.findOneAndUpdate(
    { _id: id, userId: req.user.userId },
    { title, completed },
    { new: true }
  );

  if (!todo) return res.status(404).json({ error: 'Not found' });
  res.json(todo);
});

// DELETE
router.delete('/:id', auth, async (req, res) => {
  const { id } = req.params;

  const todo = await Todo.findOneAndDelete({ _id: id, userId: req.user.userId });
  if (!todo) return res.status(404).json({ error: 'Not found' });

  res.json(todo);
});

module.exports = router;

๐Ÿš€ Step 9. Update

index.js

require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');

const authRoutes = require('./routes/auth');
const todoRoutes = require('./routes/todos');

const app = express();
app.use(cors());
app.use(express.json());

// connect MongoDB
mongoose.connect(process.env.MONGO_URI)
  .then(() => console.log('โœ… MongoDB connected'))
  .catch(err => console.error('MongoDB error', err));

// routes
app.use('/api/auth', authRoutes);
app.use('/api/todos', todoRoutes);

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`๐Ÿš€ Server running on http://localhost:${PORT}`));

๐Ÿ” Step 10. Test in Postman / Curl

Register user

POST http://localhost:3000/api/auth/register
Content-Type: application/json
{
  "username": "deepak",
  "password": "12345"
}

Login

POST http://localhost:3000/api/auth/login
Content-Type: application/json
{
  "username": "deepak",
  "password": "12345"
}

Copy the JWT token returned.

Get Todos (with pagination & filter)

GET http://localhost:3000/api/todos?completed=false&limit=5&skip=0
Authorization: Bearer <your_token>

Add Todo

POST http://localhost:3000/api/todos
Authorization: Bearer <your_token>
{
  "title": "Learn Vue with Node"
}

Update Todo

PUT http://localhost:3000/api/todos/<todoId>
Authorization: Bearer <your_token>
{
  "completed": true
}

Delete Todo

DELETE http://localhost:3000/api/todos/<todoId>
Authorization: Bearer <your_token>

๐Ÿงฎ Pagination Example Response

{
  "total": 17,
  "todos": [
    { "_id": "abc123", "title": "Task 1", "completed": false },
    { "_id": "abc124", "title": "Task 2", "completed": true }
  ]
}

You can now use limit & skip on the frontend for infinite scrolling or pagination controls.

Weโ€™ll add:

โœ… Login / Register UI
โœ… JWT storage & auto attach in API calls
โœ… Todo management per user
โœ… Pagination & filtering controls

โš™๏ธ Step 1. Folder structure (Vue)

In your client/src folder, create this structure:

src/
โ”œโ”€โ”€ api/
โ”‚   โ””โ”€โ”€ api.js
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ AuthForm.vue
โ”‚   โ””โ”€โ”€ TodoList.vue
โ”œโ”€โ”€ pages/
โ”‚   โ”œโ”€โ”€ LoginPage.vue
โ”‚   โ””โ”€โ”€ TodoPage.vue
โ”œโ”€โ”€ router/
โ”‚   โ””โ”€โ”€ index.js
โ”œโ”€โ”€ App.vue
โ””โ”€โ”€ main.js

๐Ÿ“ฆ Step 2. Install Axios & Vue Router

If not already installed:

cd client
npm i axios vue-router

๐Ÿง  Step 3. Create src/api/api.js

Centralized API helper that injects the JWT token into headers.

import axios from 'axios'

const api = axios.create({
  baseURL: 'http://localhost:3000/api'
})

// attach JWT token automatically
api.interceptors.request.use(config => {
  const token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

export default api

๐Ÿงฉ Step 4. Add Vue Router โ€” src/router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import LoginPage from '../pages/LoginPage.vue'
import TodoPage from '../pages/TodoPage.vue'

const routes = [
  { path: '/', redirect: '/todos' },
  { path: '/login', component: LoginPage },
  { path: '/todos', component: TodoPage, meta: { requiresAuth: true } }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

// Navigation guard for auth routes
router.beforeEach((to, from, next) => {
  const isLoggedIn = !!localStorage.getItem('token')
  if (to.meta.requiresAuth && !isLoggedIn) {
    next('/login')
  } else {
    next()
  }
})

export default router

๐Ÿช„ Step 5. Update src/main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './style.css'

createApp(App).use(router).mount('#app')

๐Ÿงพ Step 6. src/components/AuthForm.vue

Handles login and register.

<template>
  <div class="auth-form">
    <h2>{{ isRegister ? 'Register' : 'Login' }}</h2>

    <form @submit.prevent="submitForm">
      <input v-model="username" placeholder="Username" required />
      <input v-model="password" type="password" placeholder="Password" required />
      <button>{{ isRegister ? 'Register' : 'Login' }}</button>
    </form>

    <p class="switch">
      <span @click="isRegister = !isRegister">
        {{ isRegister ? 'Already have an account? Login' : 'New user? Register' }}
      </span>
    </p>

    <p v-if="error" class="error">{{ error }}</p>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import api from '../api/api'
import { useRouter } from 'vue-router'

const router = useRouter()
const username = ref('')
const password = ref('')
const isRegister = ref(false)
const error = ref('')

async function submitForm() {
  error.value = ''
  try {
    if (isRegister.value) {
      await api.post('/auth/register', { username: username.value, password: password.value })
      isRegister.value = false
      alert('Registered successfully! Please login.')
    } else {
      const res = await api.post('/auth/login', { username: username.value, password: password.value })
      localStorage.setItem('token', res.data.token)
      router.push('/todos')
    }
  } catch (err) {
    error.value = err.response?.data?.error || 'Something went wrong'
  }
}
</script>

<style scoped>
.auth-form {
  max-width: 400px;
  margin: 50px auto;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 0 10px rgba(0,0,0,0.1);
  text-align: center;
}
.auth-form input {
  width: 100%;
  margin: 8px 0;
  padding: 8px;
}
.auth-form button {
  width: 100%;
  padding: 8px;
}
.switch span {
  color: #42b883;
  cursor: pointer;
}
.error {
  color: red;
  margin-top: 10px;
}
</style>

๐Ÿ“„ Step 7. src/pages/LoginPage.vue

<template>
  <AuthForm />
</template>

<script setup>
import AuthForm from '../components/AuthForm.vue'
</script>

๐Ÿ“„ Step 8. src/components/TodoList.vue

Now supports:

  • Pagination

  • Filtering (completed / pending)

  • CRUD operations

<template>
  <div class="todos-container">
    <div class="header">
      <h2>Your Todos</h2>
      <button @click="logout">Logout</button>
    </div>

    <form @submit.prevent="addTodo" class="add-form">
      <input v-model="newTitle" placeholder="Add new todo..." />
      <button :disabled="!newTitle.trim()">Add</button>
    </form>

    <div class="filters">
      <label>Show:</label>
      <select v-model="filter" @change="loadTodos">
        <option value="">All</option>
        <option value="false">Pending</option>
        <option value="true">Completed</option>
      </select>
    </div>

    <ul class="todo-list">
      <li v-for="todo in todos" :key="todo._id" class="todo-item">
        <input type="checkbox" v-model="todo.completed" @change="updateTodo(todo)" />
        <span :class="{ done: todo.completed }">{{ todo.title }}</span>
        <button @click="deleteTodo(todo._id)">Delete</button>
      </li>
    </ul>

    <div class="pagination">
      <button @click="prevPage" :disabled="skip === 0">Prev</button>
      <button @click="nextPage" :disabled="skip + limit >= total">Next</button>
      <span>Showing {{ skip + 1 }} - {{ Math.min(skip + limit, total) }} / {{ total }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import api from '../api/api'
import { useRouter } from 'vue-router'

const router = useRouter()
const todos = ref([])
const newTitle = ref('')
const total = ref(0)
const skip = ref(0)
const limit = 5
const filter = ref('')

async function loadTodos() {
  try {
    const params = { skip: skip.value, limit, completed: filter.value || undefined }
    const res = await api.get('/todos', { params })
    todos.value = res.data.todos
    total.value = res.data.total
  } catch (err) {
    console.error('Load error:', err)
    if (err.response?.status === 401) logout()
  }
}

async function addTodo() {
  if (!newTitle.value.trim()) return
  await api.post('/todos', { title: newTitle.value })
  newTitle.value = ''
  await loadTodos()
}

async function updateTodo(todo) {
  await api.put(`/todos/${todo._id}`, { completed: todo.completed })
}

async function deleteTodo(id) {
  await api.delete(`/todos/${id}`)
  await loadTodos()
}

function nextPage() {
  skip.value += limit
  loadTodos()
}

function prevPage() {
  skip.value = Math.max(0, skip.value - limit)
  loadTodos()
}

function logout() {
  localStorage.removeItem('token')
  router.push('/login')
}

onMounted(loadTodos)
</script>

<style scoped>
.todos-container {
  max-width: 600px;
  margin: 30px auto;
}
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.add-form { display:flex; gap:10px; margin-top:10px; }
.filters { margin: 15px 0; }
.todo-list { list-style:none; padding:0; margin:0; }
.todo-item { display:flex; gap:10px; align-items:center; border-bottom:1px solid #eee; padding:6px 0; }
.done { text-decoration: line-through; color: gray; }
.pagination { margin-top:15px; display:flex; justify-content:space-between; align-items:center; }
</style>

๐Ÿงพ Step 9. src/pages/TodoPage.vue

<template>
  <TodoList />
</template>

<script setup>
import TodoList from '../components/TodoList.vue'
</script>

๐Ÿ Step 10. src/App.vue

<template>
  <router-view />
</template>

โ–ถ๏ธ Step 11. Run both servers

Backend

cd server
npm run dev

Frontend

cd client
npm run dev

Visit http://localhost:5173

1๏ธโƒฃ Register a user
2๏ธโƒฃ Login (stores JWT in localStorage)
3๏ธโƒฃ Add / edit / delete todos
4๏ธโƒฃ Filter completed/pending
5๏ธโƒฃ Paginate (Prev / Next buttons)

๐ŸŽฏ Summary

You now have a complete full-stack app:

โœ… Vue 3 frontend
โœ… Express + MongoDB backend
โœ… JWT authentication
โœ… Pagination & filtering
โœ… Protected user-specific todos