Lesson 4
Associating ToDos with Users in NestJS
Introduction and Overview

Welcome to the final lesson of our course on implementing Authentication in our ToDo app with NestJS. In the previous lessons, we built a strong foundation by setting up user authentication using bcrypt for password encryption, integrating JWT for secure access, and protecting our endpoints with Guards. In this lesson, we will focus on associating ToDo items with their respective users, which is crucial for maintaining data integrity and ensuring that users interact only with their own tasks.

Integrating Mongoose with NestJS for Todo Management
Creating the Todo Schema with User Association

The core of our task is to ensure each ToDo item is associated with a user. We'll achieve this by updating the Todo schema to include an ownerId property to signify which user it belongs to.

Here’s a breakdown of how we update the Todo schema:

TypeScript
1import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2import { HydratedDocument } from 'mongoose'; 3 4export type TodoDocument = HydratedDocument<Todo>; 5 6@Schema() 7export class Todo { 8 // Add this property 9 @Prop({ required: true }) 10 ownerId: string; // Associates the ToDo with a specific user 11 12 // These properties were already there. 13 @Prop({ required: true }) 14 title: string; 15 16 @Prop() 17 description: string; 18 19 @Prop({ default: false }) 20 completed: boolean; 21} 22 23export const TodoSchema = SchemaFactory.createForClass(Todo);
Explanation:
  • ownerId: This field is critical as it links each ToDo item to a user, ensuring that users can only access their own ToDo items.
  • title, description, completed: These fields store the ToDo details, enabling users to manage their tasks' status.
Updating the Todo Service to consider ToDo owners

The TodoService contains the logic for CRUD operations on ToDo items, making sure these operations consider the user-specific association. We need to update all of them to require a userId and relate ToDos with that user. This means that you can only list or update ToDos that you own. When you create a ToDo, it gets associated wit you.

Here’s a part of the TodoService implementation:

TypeScript
1@Injectable() 2export class TodoService { 3 constructor(@InjectModel(Todo.name) private readonly todoModel: Model<TodoDocument>) {} 4 5 async findAll(userId: string, showIncomplete?: boolean): Promise<Todo[]> { 6 const filter: any = { 7 ownerId: userId, 8 }; 9 10 if (showIncomplete) { 11 filter.completed = false; 12 } 13 14 return await this.todoModel.find(filter).exec(); 15 } 16 17 async findOne(userId: string, id: string): Promise<TodoDocument | null> { 18 const todo = await this.todoModel.findById(id).exec(); 19 if (todo?.ownerId !== userId) { 20 return null; 21 } 22 return todo; 23 } 24 25 async createTodo(userId: string, todo: CreateTodoDto): Promise<TodoDocument> { 26 const createdTodo = new this.todoModel({ 27 ownerId: userId, 28 ...todo, 29 }); 30 return createdTodo.save(); 31 } 32 33 // Other CRUD operations like update and delete have similar ownerId concerns... 34}
Explanation:
  • findAll and findOne: Fetches ToDos for the given userId. This method uses the ownerId to ensure only the user's ToDos are returned.
  • createTodo: This funciton ensures the ownerId is added to the record in the database
  • The service contains other methods (createTodo, updateTodo, etc.) that maintain this user association, ensuring that operations obey user-specific constraints.
Developing the Todo Controller

The TodoController handles incoming HTTP requests related to ToDo operations and communicates with the TodoService.

An example endpoint within our TodoController:

TypeScript
1function userIdFromRequest(req: any): string { 2 // The user should be set on the request object by the AuthGuard. 3 // This information was embedded within the JWT token 4 return req.user?.sub ?? ""; 5} 6 7@Controller('todos') 8export class TodoController { 9 constructor(private readonly todosService: TodoService) {} 10 11 @Get() 12 async findAll(@Request() req, @Query('showIncomplete') showIncomplete: boolean): Promise<TodoDto[]> { 13 // Find ToDos that are owned by the user that was received from the token. 14 const todos = await this.todosService.findAll(userIdFromRequest(req), showIncomplete); 15 return todos.map(transformTodoDto); 16 } 17 18 @Get(':id') 19 async findOne(@Request() req, @Param('id') id: string): Promise<TodoDto> { 20 const todo = await this.todosService.findOne(userIdFromRequest(req), id); 21 22 if (!todo) { 23 throw new NotFoundException('Todo not found'); 24 } 25 26 return transformTodoDto(todo); 27 } 28 29 @Post() 30 async create(@Request() req, @Body(new ValidationPipe()) todo: CreateTodoDto): Promise<TodoDto> { 31 const newTodo = await this.todosService.createTodo(userIdFromRequest(req), todo); 32 return transformTodoDto(newTodo); 33 } 34 35 // Other endpoints 36}
Explanation:
  • @Get(): This endpoint fetches all ToDos for the current user. It uses a helper function, userIdFromRequest, to extract the user identity from the request (which was set in the AuthGuard), ensuring tasks are fetched only for the corresponding user.
  • @Get(':id') and @Post(): These endpoints use the same mechanism to pass the user ID to the ToDoService.
  • The controller has similar logic for other HTTP operations (POST, PUT, DELETE), consistently maintaining the association with users.
Example: Working with ToDos and Users

We can see this association in action through an example using send_request.ts:

TypeScript
1// Create a new todo item 2async function createTodo(token: string, username: string) { 3 try { 4 const response = await axios.post('http://localhost:3000/todos', { 5 title: 'Learn NestJS', 6 description: `${username} Explore the basics of NestJS`, 7 }, { 8 headers: { Authorization: `Bearer ${token}` } 9 }); 10 return response.data; 11 } catch (error: any) { 12 console.error('Error:', error.message); 13 } 14} 15 16// Get all todo items 17async function getTodos(token: string) { 18 try { 19 const response = await axios.get('http://localhost:3000/todos', { 20 headers: { Authorization: `Bearer ${token}` } 21 }); 22 console.log('Todos:', response.data); 23 } catch (error: any) { 24 console.error('Error:', error.message); 25 } 26} 27 28async function run() { 29 console.log('Registering a new User 1'); 30 await registerUser("testuser1"); 31 32 console.log('\nLogging in the User 1'); 33 const { access_token: testUser1Token } = await loginUser("testuser1"); 34 35 console.log('\nCreating a new todo for User 1'); 36 await createTodo(testUser1Token, "User 1"); 37 38 console.log('\nGetting all todos for User 1'); 39 await getTodos(testUser1Token); 40}
Explanation:
  • createTodo: Adds a new task for "User 1", associating the task with the user's identity through the token.
  • getTodos: Retrieves tasks, demonstrating user-specific filtering.
Summary and Preparing for Practice

In this lesson, we explored how to associate ToDo items with users in a NestJS application, utilizing schemas, services, and controllers to maintain this link. By integrating Mongoose and leveraging its powerful schema capabilities, we ensured that each ToDo was managed securely and accurately.

As you move to the practice exercises, apply what you've learned by creating and managing your ToDos while maintaining these user associations. Congratulations on reaching the end of the course! Your understanding of secure and scalable application development with NestJS and MongoDB provides a solid foundation for future projects. Happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.