Introduction: Why Friendships Matter in Our App

Welcome to the first lesson of the course. In this lesson, we’ll introduce user friendships to the reading tracker. Friendships enable users to connect and motivate each other by sharing progress. By the end, you’ll model friendships in the in-memory store, create a dedicated friends module, and implement a secure endpoint for sending friend requests that uses the authenticated user from the JWT (no trusting IDs in the body).

Quick Recap: Project Structure and Setup

Our API already has users, books, and reading features wired through modules and services, plus a global JWT auth guard (requests are protected by default; public routes opt-out via @Public() where used). We’ll integrate friendships by:

  • Extending the in-memory data model (mock-db.ts) with friendIds and a FriendRequest collection.
  • Adding a minimal accessor in DatabaseService to read/write friend requests.
  • Creating a FriendsModule that composes existing services (notably UsersService) and derives the sender from @CurrentUser().

No extra per-route guards are needed because the global guard is already active.

Modeling Friendships: Data Changes

To support friendships, we need two additions:

  1. A friendIds: number[] array on each user to store confirmed friends.
  2. A FriendRequest collection to track pending/accepted/declined requests.
// src/database/mock-db.ts
export interface User {
  id: string;
  name: string;
  username: string;
  passwordHash: string; // hashed password (bcrypt)
  role: 'user' | 'admin';
  friendIds: string[]; // New: list of confirmed friend user IDs
}

export interface FriendRequest {
  id: string;
  senderId: string;
  recipientId: string;
  status: 'pending' | 'accepted' | 'declined';
}

// NEW: in-memory collection of friend requests
export const friendRequests: FriendRequest[] = [];

Also ensure seeded users include friendIds: [] by default, so new friendships can be recorded

Building the Friends Module

Next, we will create a new module called FriendsModule. This module will handle all friendship-related logic, including sending and managing friend requests.

Here is how we set up the module, controller, and service:

// src/friends/friends.module.ts
import { Module } from '@nestjs/common';
import { FriendsService } from './friends.service';
import { FriendsController } from './friends.controller';
import { UsersModule } from '../users/users.module';
import { ReadingModule } from '../reading/reading.module';

@Module({
  imports: [UsersModule, ReadingModule],
  controllers: [FriendsController],
  providers: [FriendsService],
})
export class FriendsModule {}

Explanation:

  • The FriendsModule imports the UsersModule so it can access user data.
  • It provides a FriendsService for business logic and a FriendsController for handling HTTP requests.

We also need to add the FriendsModule to our main app module:

// src/app.module.ts
@Module({
  imports: [UsersModule, BooksModule, ReadingModule, AuthModule, FriendsModule],
  // ...rest of the code
})
export class AppModule {}

Why this design? The friends feature stays cohesive and reuses existing building blocks (UsersService, global auth). This keeps responsibilities clear and minimizes changes elsewhere.

Creating the “Send Friend Request” Endpoint

Now, let’s build the endpoint that lets a user send a friend request to another user. This will be a POST request to /friends/request. We will make sure to validate the request so users cannot send requests to themselves, cannot send duplicate requests, and can only send requests to existing users. We’ll take the sender from the JWT via @CurrentUser() and the recipient from the body. The service will prevent self-requests, check both users exist, and reject duplicate pending requests.

Here is the code for the controller, DTO, and service:

// src/friends/dto/create-friend-request.dto.ts
import { IsUUID } from 'class-validator';

export class CreateFriendRequestDto {
  @IsUUID()
  recipientId!: string;
}

// src/friends/friends.controller.ts
import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { FriendsService } from './friends.service';
import { CreateFriendRequestDto } from './dto/create-friend-request.dto';
import { CurrentUser, TokenUser } from '../common/decorators/current-user.decorator';
import { JwtAuthGuard } from '../common/guards/jwt-auth.guard';

@Controller('friends')
@UseGuards(JwtAuthGuard)
export class FriendsController {
  constructor(private readonly friends: FriendsService) {}

  @Post('request')
  requestFriend(@CurrentUser() user: TokenUser, @Body() dto: CreateFriendRequestDto) {
    const req = this.friends.requestFriend(user.userId, dto.recipientId);
    return { success: true, data: req };
  }
}

// src/friends/friends.service.ts
import { Injectable, BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
import { DatabaseService } from '../database/database.service';
import { UsersService } from '../users/users.service';
import { ReadingService } from '../reading/reading.service';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class FriendsService {
  constructor(
    private readonly db: DatabaseService,
    private readonly users: UsersService,
    private readonly reading: ReadingService,
  ) {}

  requestFriend(senderId: string, recipientId: string) {
    if (senderId === recipientId) {
      throw new BadRequestException('Cannot send a friend request to yourself.');
    }
    this.users.findOne(senderId);
    this.users.findOne(recipientId);

    const requests = this.db.getFriendRequests();
    const alreadyPending = requests.some(
      (r) => r.senderId === senderId && r.recipientId === recipientId && r.status === 'pending',
    );
    if (alreadyPending) {
      throw new BadRequestException('A pending request already exists.');
    }

    const id = uuidv4();
    const newReq = { id, senderId, recipientId, status: 'pending' as const };
    requests.push(newReq);
    return newReq;
  }
}

Explanation:

  • The DTO (CreateFriendRequestDto) ensures the request body contains a valid recipientId.
  • The controller uses the @CurrentUser() decorator to get the sender’s ID from the token, not from the request body (for security).
  • The service checks for self-requests, user existence, and duplicate pending requests before creating a new friend request.

Key points

  • Trust the token, not the body: use @CurrentUser() for the sender.
  • Validation: reject self-requests and duplicate pendings; 404s will surface if a user doesn’t exist.
  • In-memory persistence: the request is appended to the shared friendRequests array via DatabaseService.getFriendRequests().

Example Output:
If Alice (user ID 2) sends a friend request to Bob (user ID 3), the response might look like:

{
  "id": 1,
  "senderId": 2,
  "recipientId": 3,
  "status": "pending"
}

If Alice tries to send a request to herself or send a duplicate, she will get a 400 error with a message like:

{
  "statusCode": 400,
  "message": "Cannot send a friend request to yourself."
}

or

{
  "statusCode": 400,
  "message": "A pending request already exists."
}
Testing the Endpoint with cURL

Let’s see how to test the new endpoint using cURL. Here are some example commands:

Admin login (default seed: admin/admin)

ADMIN_TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"admin"}' | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{try{let j=JSON.parse(s);console.log(j.data.access_token)}catch(e){}})")
echo "ADMIN_TOKEN: ${ADMIN_TOKEN}"
  • Notice how we store ADMIN_TOKEN as a variable which can be reused later._

Lookup Alice id (seeded: username 'alice'):

ALICE_ID=$(curl -s http://localhost:3000/users | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{let j=JSON.parse(s);let u=j.data.find(x=>x.username==='alice');if(u)console.log(u.id);})")
echo "ALICE_ID: ${ALICE_ID}"
  • This should return ALICE_ID

Admin sends friend request to Alice:

curl -s -X POST http://localhost:3000/friends/request \
  -H "Authorization: Bearer ${ADMIN_TOKEN}" \
  -H 'Content-Type: application/json' \
  -d "{\"recipientId\":\"${ALICE_ID}\"}"
  • This should return the new friend request object with status "pending".
Summary and Practice Preview

In this lesson, you learned how to model friendships in your app, set up a dedicated friends module, and build a secure endpoint for sending friend requests. You also saw how to test your endpoint and handle common errors. These are the building blocks for managing user connections in your reading tracker.

Next up (Unit 2): you’ll implement listing incoming requests and accepting/declining them with proper authorization so confirmed friendships can form.

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal