Skip to main content

Overview

Task Manager uses db-local, a lightweight file-based JSON database for Node.js. This solution provides a simple, zero-configuration database that’s perfect for development, prototyping, and small-scale applications.
db-local stores data in JSON files on the filesystem, making it easy to inspect, backup, and version control your data.

Why db-local?

Zero Configuration

No database server to install or configure. Just import and start using it.

File-Based Storage

Data is stored in JSON files, making it human-readable and easy to debug.

MongoDB-like API

Familiar query syntax with find(), findOne(), create(), update(), and remove().

Lightweight

Minimal dependencies and small footprint—perfect for learning and prototyping.

Database Setup

The database is configured in a single schema file:
import DBlocal from 'db-local';

// Create database instance and specify storage location
const { Schema } = new DBlocal({ path: './src/DB' });

// Define Tasks schema
export const Tasks = Schema('tasks', {
  _id: { type: String, required: true },
  title: { type: String, required: true },
  description: { type: String },
  completed: { type: Boolean, default: false },
  createdAt: { type: String, required: true },
  updatedAt: { type: String },
  finishedAt: { type: String },
  categoryId: { type: String, default: "" }
});

// Define TaskCategories schema
export const TaskCategories = Schema('taskCategories', {
  _id: { type: String, required: true },
  name: { type: String, required: true }
});
1

Import db-local

Import the library and create a new database instance.
2

Set Storage Path

Configure where JSON files will be stored: ./src/DB
3

Define Schemas

Create schema definitions for each collection (Tasks and TaskCategories).
4

Export Schemas

Export schema instances for use in models.
When you create a schema named 'tasks', db-local automatically creates a file at ./src/DB/tasks.json to store the data.

Schema Definitions

Tasks Schema

The Tasks schema defines the structure for task documents:
FieldTypeRequiredDefaultDescription
_idStringYes-Unique identifier (UUID)
titleStringYes-Task title (3-25 chars)
descriptionStringNo-Task description
completedBooleanNofalseCompletion status
createdAtStringYes-ISO timestamp
updatedAtStringNo-ISO timestamp
finishedAtStringNo-ISO timestamp
categoryIdStringNo""Reference to category

Example Task Document

{
  "_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "title": "Complete documentation",
  "description": "Write architecture docs",
  "completed": false,
  "createdAt": "2026-03-11T10:30:00.000Z",
  "updatedAt": "2026-03-11T11:45:00.000Z",
  "categoryId": "work-category-id"
}

TaskCategories Schema

The TaskCategories schema is simpler, with just two fields:
FieldTypeRequiredDescription
_idStringYesUnique identifier (UUID)
nameStringYesCategory name (2-25 chars)

Example Category Document

{
  "_id": "work-category-id",
  "name": "Work"
}

UUID Generation

The application uses Node.js’s built-in crypto module to generate unique identifiers:
import { randomUUID } from "node:crypto";

static create({ input }) {
  const newTask = {
    _id: randomUUID(), // Generates UUID v4
    title: input.title,
    description: input.description,
    categoryId: input.categoryId,
    createdAt: new Date().toISOString(),
    completed: input.completed,
  };
  Tasks.create(newTask).save();
  return newTask;
}
UUID v4 Characteristics:
  • Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
  • Uniqueness: 122 bits of randomness
  • Collision Probability: Negligibly small (1 in 5.3 × 10³⁶)
  • Example: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Using UUIDs instead of auto-incrementing integers provides globally unique identifiers that can be generated client-side or server-side without coordination.

CRUD Operations

db-local provides a MongoDB-like API for data operations:

Create

const newTask = {
  _id: randomUUID(),
  title: "New Task",
  description: "Task description",
  createdAt: new Date().toISOString(),
  completed: false,
};

Tasks.create(newTask).save();
The .save() method must be called to persist the data to the JSON file.

Read

// Find all tasks
const allTasks = await Tasks.find({});

// Find tasks with filter
const completedTasks = await Tasks.find({ completed: true });

// Find single task by ID
const task = await Tasks.findOne({ _id: taskId });

// Find with $in operator
const tasksInCategories = await Tasks.find({
  categoryId: { $in: ["category1", "category2"] }
});
db-local supports MongoDB-style query operators:
  • $in: Match any value in an array
    { categoryId: { $in: ["id1", "id2"] } }
    
  • Exact Match: Simple equality
    { completed: true }
    
  • Multiple Conditions: Object with multiple properties
    { completed: false, categoryId: "work" }
    

Update

// Update single task
const task = await Tasks.findOne({ _id: taskId });
if (task) {
  await task.update({
    title: "Updated Title",
    updatedAt: new Date().toISOString()
  }).save();
}

// Update multiple tasks
const tasksToUpdate = await Tasks.find({ categoryId: { $in: oldCategoryIds } });
tasksToUpdate.forEach((task) => {
  task.update({
    categoryId: newCategoryId,
    updatedAt: new Date().toISOString(),
  }).save();
});
The update() method performs a partial update, only modifying the specified fields while leaving others unchanged.

Delete

// Remove single task
Tasks.remove(taskId);

// Remove multiple tasks
const tasksToDelete = await Tasks.find({ categoryId: deletedCategoryId });
tasksToDelete.forEach((task) => {
  Tasks.remove(task._id);
});

Advanced Patterns

Batch Updates

The application implements a batch update pattern in the Task model:
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;
}
Use Case: When deleting a category, all tasks in that category need their categoryId updated to “uncategorized”. The updateMany() method handles this efficiently.

Cascading Deletes

The application implements cascading deletes through the service layer:
import { TaskModel } from "../models/task.js";
import { TaskCategoryModel } from "../models/taskCategory.js";

export class TaskCategoryService {
  static async deleteCategories(categoriesId) {
    // Step 1: Update all tasks in deleted categories
    await TaskModel.updateMany("categoryId", categoriesId, "uncategorized");
    
    // Step 2: Delete the categories
    const result = await TaskCategoryModel.delete(categoriesId);
    return result !== false;
  }
}
1

Update Related Tasks

Before deleting categories, update all tasks that reference them.
2

Delete Categories

After tasks are updated, safely delete the categories.
3

Return Status

Return success/failure status to the controller.

Timestamp Management

The application automatically manages three types of timestamps:

createdAt

Set once when the task is created.
createdAt: new Date().toISOString()
// "2026-03-11T10:30:00.000Z"

updatedAt

Updated every time the task is modified.
updatedAt: new Date().toISOString()

finishedAt

Set when a task is marked as completed.
if (newCompleted === true) {
  finishedAt: new Date().toISOString()
}

Smart Completion Handling

The Task model intelligently manages the finishedAt timestamp:
// Check if completed status is changing
if ("completed" in cleanInputData) {
  const newCompleted = cleanInputData.completed;
  const oldCompleted = !!taskExists.completed;

  // Task is being marked complete
  if (oldCompleted === false && newCompleted === true) {
    cleanInputData.finishedAt = new Date().toISOString();
  }
  
  // Task is being marked incomplete
  if (oldCompleted === true && newCompleted === false) {
    delete taskExists.finishedAt;
  }
}
Marking Task as Complete:
  • When completed changes from false to true
  • Set finishedAt to current timestamp
  • Preserves the exact moment of completion
Marking Task as Incomplete:
  • When completed changes from true to false
  • Delete the finishedAt field
  • Removes misleading completion timestamp
No Status Change:
  • If completed stays the same
  • Don’t modify finishedAt
  • Preserves original completion timestamp

Data Persistence

db-local automatically handles data persistence:
1

In-Memory Operations

Operations are performed in memory first for speed.
2

Save to Disk

Calling .save() writes changes to the JSON file.
Tasks.create(newTask).save();
task.update(changes).save();
3

File Location

Data is stored in JSON files at the configured path.
src/DB/tasks.json
src/DB/taskCategories.json
Every .save() call writes the entire collection to disk, so db-local is best suited for small to medium datasets. For large-scale applications, consider PostgreSQL, MongoDB, or MySQL.

File Structure

The database files are organized as follows:
src/
└── DB/
    ├── DB_schemas.js          # Schema definitions
    ├── tasks.json             # Task data (auto-generated)
    └── taskCategories.json    # Category data (auto-generated)
The JSON files are auto-generated by db-local when you first save data. You should add them to .gitignore if you don’t want to commit your development data.

Data Integrity

Type Validation

Schemas enforce basic type validation:
Schema('tasks', {
  _id: { type: String, required: true },    // Must be string
  title: { type: String, required: true },  // Must be string
  completed: { type: Boolean, default: false } // Must be boolean
});
While db-local provides basic type checking, the application uses Zod for comprehensive validation before data reaches the database layer.

Referential Integrity

The application maintains referential integrity through application logic:
// Before deleting categories, update all referencing tasks
await TaskModel.updateMany("categoryId", deletedCategoryIds, "uncategorized");

// Now safe to delete categories
await TaskCategoryModel.delete(deletedCategoryIds);
Unlike relational databases, db-local doesn’t enforce foreign key constraints. The application handles referential integrity in the service layer.

Query Performance

Small Datasets

db-local loads the entire collection into memory, making queries fast for small datasets (< 10,000 documents).

Large Datasets

For larger datasets, consider migrating to a proper database with indexing and query optimization.

Migration Considerations

When your application grows, you might need to migrate from db-local:
Indicators You Need to Migrate:
  • More than 10,000 records
  • Need for complex queries and joins
  • Multiple concurrent users
  • Need for transactions
  • Performance degradation
Migration Options:
  1. MongoDB: Similar API to db-local, easier transition
  2. PostgreSQL: Powerful relational database with JSON support
  3. MySQL: Traditional relational database
  4. SQLite: File-based like db-local, but more powerful
Migration Strategy:
  • Export data from JSON files
  • Create schema in new database
  • Import data
  • Update model layer to use new database client
  • Controllers and services remain mostly unchanged

Best Practices

1

Always Call .save()

After create() or update(), always call .save() to persist changes.
Tasks.create(newTask).save(); // ✅ Correct
Tasks.create(newTask);        // ❌ Not persisted
2

Use UUIDs for IDs

Generate unique identifiers with randomUUID() instead of auto-incrementing numbers.
3

Handle Timestamps

Always set createdAt on creation and updatedAt on updates.
4

Validate Before Saving

Use Zod schemas to validate data before it reaches the database layer.
5

Check Existence

Before updates and deletes, check if the document exists.
const task = await Tasks.findOne({ _id: taskId });
if (!task) return null;

Example: Complete CRUD Flow

Here’s a complete example showing all CRUD operations:
import { randomUUID } from "node:crypto";
import { Tasks } from "../DB/DB_schemas.js";

// CREATE
const newTask = {
  _id: randomUUID(),
  title: "Learn db-local",
  description: "Understand the database layer",
  completed: false,
  createdAt: new Date().toISOString(),
  categoryId: "learning"
};
Tasks.create(newTask).save();
console.log("Created:", newTask);

// READ
const allTasks = await Tasks.find({});
console.log("All tasks:", allTasks);

const task = await Tasks.findOne({ _id: newTask._id });
console.log("Single task:", task);

// UPDATE
await task.update({
  completed: true,
  finishedAt: new Date().toISOString(),
  updatedAt: new Date().toISOString()
}).save();
console.log("Updated task");

// DELETE
Tasks.remove(newTask._id);
console.log("Deleted task");
This example demonstrates the complete lifecycle of a task document, from creation to deletion, showcasing all the database operations available in Task Manager.

Build docs developers (and LLMs) love