Overview
The Task Manager includes a powerful category system that allows tasks to be organized and filtered. When categories are deleted, the system automatically reassigns associated tasks to prevent data loss.
Category Data Model
Categories have a simple structure:
interface TaskCategory {
_id : string ; // UUID
name : string ; // 2-25 characters
}
Categories are intentionally lightweight, focusing on a simple name-based organization system.
Creating Categories
Backend Implementation
~/workspace/source/src/models/taskCategory.js
import { randomUUID } from "node:crypto" ;
import { TaskCategories } from "../DB/DB_schemas.js" ;
export class TaskCategoryModel {
static create ({ input }) {
try {
const newCategory = {
_id: randomUUID (),
name: input . name ,
};
TaskCategories . create ( newCategory ). save ();
return newCategory ;
} catch ( error ) {
throw error ;
}
}
}
Validation Schema
Category names are validated with Zod:
~/workspace/source/src/schemas/taskCategory.js
import z from 'zod' ;
const taskCategorySchema = z . object ({
name: z . string (). min ( 2 ). max ( 25 )
});
export function validateTaskCategory ( input ) {
return taskCategorySchema . safeParse ( input );
}
Category names must be between 2 and 25 characters. The validation ensures consistency across the application.
Controller with Error Handling
~/workspace/source/src/controllers/taskCategory.js
import { TaskCategoryModel } from "../models/taskCategory.js" ;
import { validateTaskCategory } from "../schemas/taskCategory.js" ;
export class TaskCategoryController {
static async create ( req , res ) {
const validate = validateTaskCategory ( req . body );
if ( validate . error ) {
res . status ( 400 ). json ( validate . error );
return ;
}
try {
const newCategory = TaskCategoryModel . create ({ input: validate . data });
res . status ( 201 ). json ( newCategory );
} catch ( error ) {
console . error ( "[CategoryController Error]:" , error );
return res . status ( 500 ). json ({
error: "No se pudo crear la categoría en el servidor"
});
}
}
}
Reading Categories
Get All Categories
~/workspace/source/src/models/taskCategory.js
static async getAll ( filters = {}) {
return await TaskCategories . find ( filters );
}
Get Category by ID
~/workspace/source/src/models/taskCategory.js
static async getById ( taskCategoryId ) {
return await TaskCategories . findOne ({ _id: taskCategoryId });
}
Controller - Get All
Controller - Get by ID
static async getAll ( req , res ) {
const categories = await TaskCategoryModel . getAll ();
res . status ( 200 ). json ( categories );
}
Updating Categories
~/workspace/source/src/models/taskCategory.js
static async update ( taskCategoryId , input ) {
const categoryExists = await this . getById ( taskCategoryId );
if ( ! categoryExists ) {
return null ;
}
await categoryExists . update ( input ). save ();
const categoryUpdated = await this . getById ( taskCategoryId );
return categoryUpdated ;
}
The controller validates input and handles errors:
~/workspace/source/src/controllers/taskCategory.js
static async update ( req , res ) {
const validate = validateTaskCategory ( req . body );
if ( validate . error ) {
res . status ( 400 ). json ({ error: "Datos no validos o incompletos" });
return ;
}
const { id } = req . params ;
const updatedCategory = await TaskCategoryModel . update ( id , validate . data );
if ( ! updatedCategory ) {
res . status ( 404 ). json ({ message: "Category not found" });
return ;
}
res . status ( 200 ). json ( updatedCategory );
}
Deleting Categories
The Challenge: Orphaned Tasks
When a category is deleted, all tasks associated with that category could become orphaned. The application solves this with automatic task reassignment.
Two-Step Deletion Process
Reassign Tasks
Find all tasks in the deleted categories and reassign them to “uncategorized”
Delete Categories
Remove the category records from the database
Service Layer Implementation
The TaskCategoryService orchestrates the deletion process:
~/workspace/source/src/services/taskCategory.js
import { TaskModel } from "../models/task.js" ;
import { TaskCategoryModel } from "../models/taskCategory.js" ;
export class TaskCategoryService {
static async deleteCategories ( categoriesId ) {
// Find tasks with these categories and set categoryId to "uncategorized"
await TaskModel . updateMany ( "categoryId" , categoriesId , "uncategorized" );
// Delete the categories
const result = await TaskCategoryModel . delete ( categoriesId );
return result !== false ;
}
}
Tasks are automatically moved to the “uncategorized” category, not deleted. This ensures no task data is lost during category deletion.
Bulk Category Deletion
The model supports deleting multiple categories at once:
~/workspace/source/src/models/taskCategory.js
static async delete ( taskCategoriesId ) {
const categoriesToDelete = await TaskCategories . find ({ "_id" : { $in: taskCategoriesId }});
if ( ! categoriesToDelete ) return false ;
try {
categoriesToDelete . forEach (( category ) => {
TaskCategories . remove ( category . _id );
});
} catch ( error ) {
return error ;
}
}
Controller Endpoint
~/workspace/source/src/controllers/taskCategory.js
static async delete ( req , res ) {
const { categoriesIds } = req . body ;
if ( ! Array . isArray ( categoriesIds )) {
return res . status ( 400 ). json ({ error: "Datos no validos, se esperaba un array" });
}
try {
const result = await TaskCategoryService . deleteCategories ( categoriesIds );
if ( result === false ) {
return res . status ( 404 ). json ({ message: "Categories not found" });
}
res . status ( 204 ). end ();
} catch ( error ) {
console . error ( "Error en CategoryController.delete:" , error );
return res . status ( 500 ). json ({
message: "Error interno al procesar el borrado masivo"
});
}
}
Why Array-Based Deletion?
The API accepts an array of category IDs to enable efficient bulk deletion operations. This is particularly useful in the UI where users can select multiple categories to delete at once.
The “Uncategorized” Category
Special Category Behavior
The application includes a special “uncategorized” category:
ID : "uncategorized"
Purpose : Default category for tasks without an explicit category
Cannot be deleted : This is a system category
Always visible : Appears first in category lists
Frontend Implementation
The frontend automatically includes the “uncategorized” category:
~/workspace/source/public/js/controllers/Task.js
async getPreparedCategory () {
const categories = await this . taskCategoryService . getAll ();
let orderCategories = [];
if ( categories . length > 0 ) {
orderCategories = sortCollection ( categories , this . sortCategoriesBy , this . sortCategoriesDirection );
}
// Add "Sin categoría" (Uncategorized) at the beginning
orderCategories . unshift ({ _id: 'uncategorized' , name: 'Sin categoría' });
return orderCategories ;
}
The “uncategorized” category acts as a safety net, ensuring every task always belongs to at least one category.
Category-Task Relationship
Task Update During Deletion
The updateMany method in TaskModel handles bulk reassignment:
~/workspace/source/src/models/task.js
static async updateMany ( propiedad , values , newValue ) {
const filterQuery = { [propiedad]: { $in: values } };
const tasksToUpdate = await Tasks . find ( filterQuery );
if ( tasksToUpdate . length === 0 ) return 0 ;
tasksToUpdate . forEach (( task ) => {
task
. update ({
[propiedad]: newValue ,
updatedAt: new Date (). toISOString (),
})
. save ();
});
return tasksToUpdate . length ;
}
Data Flow Diagram
Normal Deletion
Error Handling
Frontend Integration
Custom Events for Synchronization
The frontend uses custom events to keep the UI synchronized:
~/workspace/source/public/js/controllers/TaskCategory.js
async addCategory () {
const categoryName = document . getElementById ( "add-category-input" ). value ;
if ( ! categoryName ) return ;
try {
const newCategory = await this . categoryService . save ( categoryName );
// Dispatch custom event to notify task view
const event = new CustomEvent ( 'categoryUpdated' , {
detail: { action: 'create' , data: newCategory }
});
window . dispatchEvent ( event );
this . init ();
} catch ( error ) {
console . error ( "Error al añadir categoría:" , error . message );
alert ( `No se pudo guardar: ${ error . message } ` );
}
}
Listening for Category Changes
~/workspace/source/public/js/controllers/Task.js
addEventListeners () {
this . container . addEventListener ( 'click' , this . handleClickEvent . bind ( this ));
window . addEventListener ( 'modal:confirm' , ( e ) => this . handleModalEvent ( e ));
// Listen for category updates and refresh task view
window . addEventListener ( 'categoryUpdated' , ( e ) => this . init ());
}
The custom event pattern ensures the task list automatically refreshes when categories are added, updated, or deleted, keeping the UI consistent.
API Routes
~/workspace/source/src/routes/taskCategories.js
import { Router } from "express" ;
import { TaskCategoryController } from "../controllers/taskCategory.js" ;
export const taskCategoriesRouter = Router ();
taskCategoriesRouter . get ( "/" , TaskCategoryController . getAll );
taskCategoriesRouter . get ( "/:id" , TaskCategoryController . getById );
taskCategoriesRouter . post ( "/" , TaskCategoryController . create );
taskCategoriesRouter . post ( "/delete" , TaskCategoryController . delete );
taskCategoriesRouter . patch ( "/:id" , TaskCategoryController . update );
The delete endpoint uses POST instead of DELETE to support passing an array of category IDs in the request body.
Error Handling
400 Bad Request Invalid category name or expected array not provided
404 Not Found Category ID does not exist
500 Internal Server Error Database or bulk operation failure
201 Created Category successfully created
Best Practices
Cascading Operations : Always use the service layer for deletions to ensure tasks are properly reassigned.
Validation : Enforce the 2-25 character limit on both frontend and backend to maintain data consistency.
Bulk Operations : When deleting multiple categories, send them in a single request to reduce network overhead and ensure atomic operations.
Never directly delete categories from the model layer - always go through the service layer to prevent orphaned tasks.
Next Steps
Task Management Learn about task CRUD operations and timestamps
Frontend UI Explore how categories are displayed in the UI