Back to Blog
Full Stack Development

Building a Real-Time Task Management App with React and Node.js

A task management app is the perfect full stack project. It requires user authentication, CRUD operations, a clean UI, and — if you want to take it further — real-time updates. It is simple enough for a beginner to build in a week, yet extensible enough to demonstrate advanced concepts.

In this tutorial, we will build a complete task management app (like a mini Trello) with a React frontend, a Node.js + Express backend, and PostgreSQL for storage. We will add real-time updates using Socket.io so that when one user adds a task, everyone connected sees it instantly.

Project Structure

We will use a monorepo structure for simplicity:

task-manager/
  client/          # React frontend (Vite)
  server/          # Node.js + Express backend
  .gitignore
  README.md

Part 1: Backend Setup

Set up the server as covered in our REST API tutorial. Create a tasks table in PostgreSQL:

CREATE TABLE tasks (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  title TEXT NOT NULL,
  description TEXT,
  status TEXT DEFAULT 'todo' CHECK (status IN ('todo', 'in_progress', 'done')),
  priority TEXT DEFAULT 'medium' CHECK (priority IN ('low', 'medium', 'high')),
  user_id UUID REFERENCES users(id) NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Part 2: Task API Endpoints

Create the routes for task CRUD:

// server/routes/tasks.js
const express = require('express');
const db = require('../db');
const auth = require('../middleware/auth');

const router = express.Router();
router.use(auth);

router.get('/', async (req, res) => {
  const result = await db.query(
    'SELECT * FROM tasks WHERE user_id = $1 ORDER BY created_at DESC',
    [req.userId]
  );
  res.json(result.rows);
});

router.post('/', async (req, res) => {
  const { title, description, priority } = req.body;
  const result = await db.query(
    `INSERT INTO tasks (title, description, priority, user_id)
     VALUES ($1, $2, $3, $4) RETURNING *`,
    [title, description, priority || 'medium', req.userId]
  );
  const task = result.rows[0];
  req.app.get('io').emit('task:created', task);
  res.status(201).json(task);
});

router.put('/:id', async (req, res) => {
  const { title, description, status, priority } = req.body;
  const result = await db.query(
    `UPDATE tasks SET title = COALESCE($1, title),
     description = COALESCE($2, description),
     status = COALESCE($3, status),
     priority = COALESCE($4, priority),
     updated_at = NOW()
     WHERE id = $5 AND user_id = $6 RETURNING *`,
    [title, description, status, priority, req.params.id, req.userId]
  );
  if (!result.rows.length) return res.status(404).json({ error: 'Task not found' });
  req.app.get('io').emit('task:updated', result.rows[0]);
  res.json(result.rows[0]);
});

router.delete('/:id', async (req, res) => {
  const result = await db.query(
    'DELETE FROM tasks WHERE id = $1 AND user_id = $2 RETURNING id',
    [req.params.id, req.userId]
  );
  if (!result.rows.length) return res.status(404).json({ error: 'Task not found' });
  req.app.get('io').emit('task:deleted', { id: req.params.id });
  res.json({ message: 'Task deleted' });
});

module.exports = router;

Notice the req.app.get('io') calls — this is where we emit real-time events. We will set up Socket.io in the main server file.

Part 3: Add Real-Time with Socket.io

Install Socket.io on the server:

npm install socket.io

Update your server/index.js to use Socket.io:

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');

const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: '*' } });

app.use(cors());
app.use(express.json());
app.set('io', io);

// ... routes ...

io.on('connection', (socket) => {
  console.log('Client connected:', socket.id);
  socket.on('disconnect', () => {
    console.log('Client disconnected:', socket.id);
  });
});

server.listen(3000, () => {
  console.log('Server running on port 3000 with WebSocket support');
});

Part 4: React Frontend

Create the React app with Vite:

npm create vite@latest client -- --template react
cd client
npm install axios socket.io-client react-router-dom

Create a components folder and build the following components:

Auth Component

A simple login/register form that stores the JWT token in localStorage after successful authentication.

TaskList Component

Displays tasks grouped by status columns (To Do, In Progress, Done). Users can drag and drop tasks between columns to update their status.

TaskForm Component

A form to add new tasks with title, description, and priority fields.

Part 5: Real-Time Frontend Integration

In your main App component, connect to Socket.io:

import { useEffect, useState } from 'react';
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000');

function App() {
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    fetchTasks();

    socket.on('task:created', (task) => {
      setTasks(prev => [task, ...prev]);
    });

    socket.on('task:updated', (updated) => {
      setTasks(prev => prev.map(t =>
        t.id === updated.id ? updated : t
      ));
    });

    socket.on('task:deleted', ({ id }) => {
      setTasks(prev => prev.filter(t => t.id !== id));
    });

    return () => {
      socket.off('task:created');
      socket.off('task:updated');
      socket.off('task:deleted');
    };
  }, []);

  async function fetchTasks() {
    const token = localStorage.getItem('token');
    const res = await axios.get('/api/tasks', {
      headers: { Authorization: `Bearer ${token}` }
    });
    setTasks(res.data);
  }

  // ... render ...
}

Now when any user creates or updates a task, all other connected users see the change instantly without refreshing the page.

Part 6: Drag and Drop with React

Install @dnd-kit/core and @dnd-kit/sortable to add drag and drop functionality:

npm install @dnd-kit/core @dnd-kit/sortable

Create three columns (To Do, In Progress, Done). When a task is dropped into a different column, call the PUT API to update its status. The real-time events will sync the change to all users.

Part 7: Deployment

Deploy the backend on Render and the frontend on Vercel. Set the environment variables on each platform:

Update the Socket.io connection URL to point to your deployed backend in production.

Extensions and Next Steps

This project can be extended in many ways:

Conclusion

This task management app covers the complete full stack: React frontend with drag and drop, Node.js + Express API with authentication, PostgreSQL database, and real-time updates with WebSockets. It is a portfolio-ready project that demonstrates every major skill a full stack developer needs.

Building this project end to end will teach you more than months of video tutorials. Start coding, make mistakes, and iterate. Need help or want to collaborate on a project? Contact Aarti Tech Services for development support, mentorship, or custom project development.

React Node.js Project Full Stack Web App