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
- Project Overview — We'll build a full-stack Task Manager with features: create tasks, set due dates, assign priority/status, add descriptions, and view a dashboard. The backend uses NestJS + Prisma + PostgreSQL. The frontend can be React or React Native. This is the kind of app companies use in interviews to evaluate candidates.
- Database Schema Design — Start with two models: User (id, email, password, name, createdAt) and Task (id, title, description, dueDate, priority, status, assigneeId, createdAt, updatedAt). The Task has a foreign key to User. Priority is an enum: LOW, MEDIUM, HIGH, URGENT. Status is an enum: TODO, IN_PROGRESS, COMPLETED. Always think about your data shape before writing any code.
- Prisma Schema & Migration — Define your models in schema.prisma with @id, @default, @relation, and enum types. Run 'npx prisma migrate dev --name init' to create the database tables. Then run 'npx prisma generate' to create the type-safe client. Prisma gives you autocompletion for every query — no raw SQL needed.
- NestJS Module Structure — Organize into feature modules: AuthModule (login/register), UsersModule (user CRUD), TasksModule (task CRUD). Each module has its own controller, service, and DTOs. Use the CLI: 'nest g module tasks', 'nest g controller tasks', 'nest g service tasks'. Keep modules focused on one responsibility.
- Authentication Flow — Register: hash password with bcrypt, store user, return JWT. Login: verify email/password, return access + refresh tokens. Protect routes with JwtAuthGuard. Use @CurrentUser() decorator to get the logged-in user. Every task operation should be scoped to the authenticated user — users can only see and modify their own tasks.
- CRUD Endpoints — POST /tasks — create a task (validate with CreateTaskDto). GET /tasks — list user's tasks with filters (status, priority, search). GET /tasks/:id — get single task (check ownership). PATCH /tasks/:id — update task fields (partial update with UpdateTaskDto). DELETE /tasks/:id — soft delete or hard delete. Always return appropriate HTTP status codes: 201 for create, 200 for success, 204 for delete.
- Filtering, Sorting & Pagination — Real apps need query features. Accept query params: ?status=TODO&priority=HIGH&search=homepage&sort=dueDate&order=asc&page=1&limit=10. Build a Prisma 'where' clause dynamically. Use skip/take for pagination. Return metadata: { data: Task[], total: number, page: number, lastPage: number }. This is what makes your API production-ready.
- Validation & Error Handling — Use class-validator decorators on DTOs: @IsString(), @IsEnum(Priority), @IsOptional(), @IsDateString(). Apply ValidationPipe globally. Create custom exceptions: TaskNotFoundException, UnauthorizedTaskAccessException. Use an exception filter to format error responses consistently with { statusCode, message, error } shape.
- Frontend Integration — The React/React Native app calls your API. Store JWT in secure storage. Create screens: Login, Register, Dashboard (task list), Create Task, Task Details. Use React Query or SWR for data fetching with automatic cache invalidation. The dashboard shows tasks grouped by status with color-coded priority badges — just like the design mockup.
- Deployment Checklist — Before shipping: add rate limiting (ThrottlerModule), enable CORS for your frontend domain, set up environment variables for production, add Swagger docs (@nestjs/swagger), write at least integration tests for auth and CRUD flows, Dockerize with a multi-stage build. A deployed project on your portfolio is worth more than 10 tutorial certificates.
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. Comment: Prisma Schema section
- 2. Priority enum declaration
- 3. LOW priority level
- 4. MEDIUM priority level
- 5. HIGH priority level
- 6. URGENT priority level
- 7. Close Priority enum
- 8.
- 9. Status enum declaration
- 10. TODO — task not started
- 11. IN_PROGRESS — task being worked on
- 12. COMPLETED — task finished
- 13. Close Status enum
- 14.
- 15. User model — stores account info
- 16. cuid() auto-generated unique ID
- 17. Unique email for login
- 18. Hashed password (never store plain text!)
- 19. User display name
- 20. One-to-many: user owns many tasks
- 21. Auto-timestamp on creation
- 22. Close User model
- 23.
- 24. Task model — the core entity
- 25. Auto-generated unique task ID
- 26. Task title — required field
- 27. Optional longer description
- 28. Optional due date
- 29. Priority defaults to MEDIUM
- 30. Status defaults to TODO
- 31. Relation: task belongs to a user (assignee)
- 32. Foreign key linking to User.id
- 33. Creation timestamp
- 34. Auto-updated on every change
- 35. Close Task model
- 36.
- 37. Comment: NestJS Tasks Controller section
- 38. Controller with 'tasks' route prefix
- 39. JwtAuthGuard protects ALL routes
- 40. Export the controller class
- 41. Inject TasksService via constructor
- 42.
- 43. POST /tasks — create a new task
- 44. Get user from JWT, validate body with DTO
- 45. Delegate to service with user.id for ownership
- 46. Close create method
- 47.
- 48. GET /tasks — list user's tasks
- 49. findAll method with query filters
- 50. Get authenticated user from decorator
- 51. Parse query params for filtering
- 52. Close params
- 53. Delegate to service with userId and filters
- 54. Close findAll method
- 55.
- 56. PATCH /tasks/:id — partial update
- 57. update method signature
- 58. Authenticated user for ownership check
- 59. Extract task ID from URL param
- 60. Validated update body
- 61. Close params
- 62. Delegate update to service
- 63. Close update method
- 64.
- 65. DELETE /tasks/:id — remove a task
- 66. Return 204 No Content on success
- 67. remove method with user and task id
- 68. Delegate deletion to service
- 69. Close remove method
- 70. Close TasksController class
- 71.
- 72. Comment: Tasks Service section
- 73. @Injectable marks this as a provider
- 74. Export the service class
- 75. Inject PrismaService for database access
- 76.
- 77. Async findAll with userId and query filters
- 78. Destructure filters with defaults for page/limit
- 79. Build dynamic Prisma where clause
- 80. Always scope to authenticated user
- 81. Conditionally add status filter
- 82. Conditionally add priority filter
- 83. Conditionally add text search
- 84. OR: match title or description
- 85. Case-insensitive title search
- 86. Case-insensitive description search
- 87. Close OR array
- 88. Close search condition
- 89. Close where clause
- 90.
- 91. Run query + count in parallel with Promise.all
- 92. findMany with where, orderBy, skip, take
- 93. Apply where clause
- 94. Dynamic sort field and direction
- 95. Skip for pagination offset
- 96. Take = page size limit
- 97. Close findMany
- 98. Count total matching records
- 99. Close Promise.all
- 100.
- 101. Return paginated response with metadata
- 102. Close findAll method
- 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
- NestJS CRUD Tutorial (NestJS)
- Prisma Relations Guide (Prisma)
- NestJS Authentication Guide (NestJS)