Overview
โ๏ธ Step 1. Install MongoDB
You have two options:
Option A: Local MongoDB
Download MongoDB Community Server
Install it, start the MongoDB service, and verify:
mongo --version
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:
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:
<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