Lesson 48 of 49 advanced

Build a Task Manager App

Your First Real Full-Stack Project

Open interactive version (quiz + challenge)

Real-world analogy

Building a Task Manager is like constructing a house. First you lay the foundation (database schema), then frame the walls (API endpoints), wire the electricity (authentication), and finally paint and decorate (the mobile/web UI). Each step builds on the last — skip one, and the whole thing wobbles.

What is it?

A Task Manager is one of the most practical full-stack projects you can build. It covers authentication, CRUD operations, database relationships, filtering, validation, and frontend integration — all the skills employers look for. Unlike todo-list tutorials, this version includes real features: priority levels, status tracking, due dates, user assignment, and a polished dashboard UI.

Real-world relevance

Jira, Trello, Asana, ClickUp, Linear — every tech company uses a task management tool. Building one teaches you the exact patterns used in production apps: user-scoped data, role-based access, complex queries, real-time updates, and responsive UI. This single project demonstrates you can build a complete product from scratch.

Key points

Code example

// ── Prisma Schema ──
enum Priority {
  LOW
  MEDIUM
  HIGH
  URGENT
}

enum Status {
  TODO
  IN_PROGRESS
  COMPLETED
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  name      String
  tasks     Task[]   @relation("assignee")
  createdAt DateTime @default(now())
}

model Task {
  id          String   @id @default(cuid())
  title       String
  description String?
  dueDate     DateTime?
  priority    Priority @default(MEDIUM)
  status      Status   @default(TODO)
  assignee    User     @relation("assignee", fields: [assigneeId], references: [id])
  assigneeId  String
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

// ── NestJS Tasks Controller ──
@Controller('tasks')
@UseGuards(JwtAuthGuard)
export class TasksController {
  constructor(private readonly tasksService: TasksService) {}

  @Post()
  create(@CurrentUser() user: User, @Body() dto: CreateTaskDto) {
    return this.tasksService.create(user.id, dto);
  }

  @Get()
  findAll(
    @CurrentUser() user: User,
    @Query() query: FilterTasksDto,
  ) {
    return this.tasksService.findAll(user.id, query);
  }

  @Patch(':id')
  update(
    @CurrentUser() user: User,
    @Param('id') id: string,
    @Body() dto: UpdateTaskDto,
  ) {
    return this.tasksService.update(user.id, id, dto);
  }

  @Delete(':id')
  @HttpCode(204)
  remove(@CurrentUser() user: User, @Param('id') id: string) {
    return this.tasksService.remove(user.id, id);
  }
}

// ── Tasks Service (with filtering) ──
@Injectable()
export class TasksService {
  constructor(private prisma: PrismaService) {}

  async findAll(userId: string, query: FilterTasksDto) {
    const { status, priority, search, sort, order, page = 1, limit = 10 } = query;
    const where: Prisma.TaskWhereInput = {
      assigneeId: userId,
      ...(status && { status }),
      ...(priority && { priority }),
      ...(search && {
        OR: [
          { title: { contains: search, mode: 'insensitive' as const } },
          { description: { contains: search, mode: 'insensitive' as const } },
        ],
      }),
    };

    const [data, total] = await Promise.all([
      this.prisma.task.findMany({
        where,
        orderBy: { [sort || 'createdAt']: order || 'desc' },
        skip: (page - 1) * limit,
        take: limit,
      }),
      this.prisma.task.count({ where }),
    ]);

    return { data, total, page, lastPage: Math.ceil(total / limit) };
  }
}

Line-by-line walkthrough

  1. 1. Comment: Prisma Schema section
  2. 2. Priority enum declaration
  3. 3. LOW priority level
  4. 4. MEDIUM priority level
  5. 5. HIGH priority level
  6. 6. URGENT priority level
  7. 7. Close Priority enum
  8. 8.
  9. 9. Status enum declaration
  10. 10. TODO — task not started
  11. 11. IN_PROGRESS — task being worked on
  12. 12. COMPLETED — task finished
  13. 13. Close Status enum
  14. 14.
  15. 15. User model — stores account info
  16. 16. cuid() auto-generated unique ID
  17. 17. Unique email for login
  18. 18. Hashed password (never store plain text!)
  19. 19. User display name
  20. 20. One-to-many: user owns many tasks
  21. 21. Auto-timestamp on creation
  22. 22. Close User model
  23. 23.
  24. 24. Task model — the core entity
  25. 25. Auto-generated unique task ID
  26. 26. Task title — required field
  27. 27. Optional longer description
  28. 28. Optional due date
  29. 29. Priority defaults to MEDIUM
  30. 30. Status defaults to TODO
  31. 31. Relation: task belongs to a user (assignee)
  32. 32. Foreign key linking to User.id
  33. 33. Creation timestamp
  34. 34. Auto-updated on every change
  35. 35. Close Task model
  36. 36.
  37. 37. Comment: NestJS Tasks Controller section
  38. 38. Controller with 'tasks' route prefix
  39. 39. JwtAuthGuard protects ALL routes
  40. 40. Export the controller class
  41. 41. Inject TasksService via constructor
  42. 42.
  43. 43. POST /tasks — create a new task
  44. 44. Get user from JWT, validate body with DTO
  45. 45. Delegate to service with user.id for ownership
  46. 46. Close create method
  47. 47.
  48. 48. GET /tasks — list user's tasks
  49. 49. findAll method with query filters
  50. 50. Get authenticated user from decorator
  51. 51. Parse query params for filtering
  52. 52. Close params
  53. 53. Delegate to service with userId and filters
  54. 54. Close findAll method
  55. 55.
  56. 56. PATCH /tasks/:id — partial update
  57. 57. update method signature
  58. 58. Authenticated user for ownership check
  59. 59. Extract task ID from URL param
  60. 60. Validated update body
  61. 61. Close params
  62. 62. Delegate update to service
  63. 63. Close update method
  64. 64.
  65. 65. DELETE /tasks/:id — remove a task
  66. 66. Return 204 No Content on success
  67. 67. remove method with user and task id
  68. 68. Delegate deletion to service
  69. 69. Close remove method
  70. 70. Close TasksController class
  71. 71.
  72. 72. Comment: Tasks Service section
  73. 73. @Injectable marks this as a provider
  74. 74. Export the service class
  75. 75. Inject PrismaService for database access
  76. 76.
  77. 77. Async findAll with userId and query filters
  78. 78. Destructure filters with defaults for page/limit
  79. 79. Build dynamic Prisma where clause
  80. 80. Always scope to authenticated user
  81. 81. Conditionally add status filter
  82. 82. Conditionally add priority filter
  83. 83. Conditionally add text search
  84. 84. OR: match title or description
  85. 85. Case-insensitive title search
  86. 86. Case-insensitive description search
  87. 87. Close OR array
  88. 88. Close search condition
  89. 89. Close where clause
  90. 90.
  91. 91. Run query + count in parallel with Promise.all
  92. 92. findMany with where, orderBy, skip, take
  93. 93. Apply where clause
  94. 94. Dynamic sort field and direction
  95. 95. Skip for pagination offset
  96. 96. Take = page size limit
  97. 97. Close findMany
  98. 98. Count total matching records
  99. 99. Close Promise.all
  100. 100.
  101. 101. Return paginated response with metadata
  102. 102. Close findAll method
  103. 103. Close TasksService class

Spot the bug

@Controller('tasks')
@UseGuards(JwtAuthGuard)
export class TasksController {
  constructor(private tasksService: TasksService) {}

  @Get()
  findAll(@Query() query: FilterTasksDto) {
    return this.tasksService.findAll(query);
  }

  @Patch(':id')
  update(@Body() dto: UpdateTaskDto, @Param('id') id: string) {
    return this.tasksService.update(id, dto);
  }
}
Need a hint?
Two critical security issues: who owns these tasks?
Show answer
1) findAll() doesn't pass the authenticated user's ID — any user can see ALL tasks in the database. Fix: add @CurrentUser() user and pass user.id to the service. 2) update() doesn't verify ownership — any authenticated user can modify any task. Fix: add @CurrentUser() user and pass user.id to the service, which should check assigneeId === userId before updating.

Explain like I'm 5

Imagine you have a big whiteboard where you write down everything you need to do — like homework, chores, and fun stuff. Now imagine that whiteboard is on your phone, and it's smart! It knows which things are super important (URGENT!), which ones are almost done, and it only shows YOUR tasks — not your sister's. That's what we're building: a smart, personal to-do whiteboard that lives on the internet!

Fun fact

Jira — the most popular task manager in tech — was originally built in 2002 by two Australians who funded it with $10,000 on credit cards. The name comes from 'Gojira' (the Japanese name for Godzilla). Today, Atlassian (Jira's parent company) is worth over $50 billion. Your Task Manager might not become Jira, but the skills you learn building it are identical to what Jira engineers use daily.

Hands-on challenge

Build the Task Manager! Start with the Prisma schema (User + Task models with enums). Generate a migration. Then create a TasksModule with full CRUD endpoints. Add JWT authentication so users only see their own tasks. Bonus: add filtering by status and priority with pagination. Deploy it and add it to your portfolio!

More resources

Open interactive version (quiz + challenge) ← Back to course: Full-Stack Playbook