DEV Community

Cover image for Building Web Push Notification Server with Nest Js and FCM
Emmanuel Ayinde
Emmanuel Ayinde

Posted on

Building Web Push Notification Server with Nest Js and FCM

Welcome to the second part of our push notification series! In this article, we'll dive into creating a Nest Js server capable of sending push notifications to our React web application. We'll cover the setup, implementation, and even extend our server to handle multiple tokens and topic notifications.

Table of Contents

  1. Introduction
  2. Project Setup
  3. Project Structure
  4. Implementing the Server
  5. Testing the Server
  6. Connecting with the React Frontend
  7. Next Steps
  8. Conclusion

Introduction

In this article, we'll build a NestJS server that can send push notifications to a React web application. We'll use Firebase Cloud Messaging (FCM) as our push notification service. By the end of this tutorial, you'll have a fully functional server capable of sending notifications to single devices, multiple devices, and topic subscribers.

Project Setup

Installing NestJS CLI

First, let's ensure we have the NestJS CLI installed. The CLI helps us create and manage NestJS projects easily.

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

This command installs the NestJS CLI globally on your machine.

Creating a New NestJS Project

Now, let's create a new NestJS project:

nest new push-notification-server
cd push-notification-server
Enter fullscreen mode Exit fullscreen mode

This creates a new NestJS project named "push-notification-server" and navigates into the project directory.

Installing Dependencies

We need to install some additional dependencies for our project:

npm install firebase-admin dotenv @nestjs/swagger
Enter fullscreen mode Exit fullscreen mode

Here's what each package does:

  • firebase-admin: Allows us to interact with Firebase services, including FCM.
  • dotenv: Helps us manage environment variables.
  • @nestjs/swagger: Provides tools for generating API documentation.

Project Structure

Our project will have the following key files:

Project Structure

  1. src/main.ts: The entry point of our application
  2. src/app.module.ts: The root module of our application
  3. src/app.service.ts: The service containing our push notification logic
  4. src/app.controller.ts: The controller containing our push notification endpoints
  5. src/dto/notification.dto.ts: The DTO file containing our data transfer objects

Let's go through each file and implement our push notification server.

Implementing the Server

Main Application File

// src/main.ts

import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle("Push Notification API")
    .setDescription("The Push Notification API description")
    .setVersion("1.0")
    .addTag("Push Notification with FCM")
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup("api/docs", app, document);

  await app.listen(9000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Let's break down this file:

  1. We import necessary modules from NestJS and Swagger.
  2. The bootstrap function is the entry point of our application.
  3. We create a NestJS application instance with NestFactory.create(AppModule).
  4. We set up Swagger for API documentation using DocumentBuilder.
  5. We create a Swagger document and set it up at the "/api/docs" endpoint.
  6. Finally, we start the server on port 9000.

App Module with Firebase Configuration

// src/app.module.ts

import { Module } from "@nestjs/common";
import * as admin from "firebase-admin";
import { config } from "dotenv";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";

config();

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {
  constructor() {
    admin.initializeApp({
      credential: admin.credential.cert({
        projectId: process.env.FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
      }),
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

This file sets up our main application module:

  1. We import necessary modules and services.
  2. The config() call loads environment variables from a .env file.
  3. We define our AppModule with its controllers and providers.
  4. In the constructor, we initialize Firebase Admin SDK with credentials from our environment variables.

Note: Make sure to set up your Firebase project and add the necessary environment variables (FIREBASE_PROJECT_ID, FIREBASE_CLIENT_EMAIL, FIREBASE_PRIVATE_KEY) in a .env file in your project root. You can refer back to our previous episode of this series to learn how to create your firebase project and retrieve the necessary keys.

App Service

// src/app.service.ts

import { Injectable } from "@nestjs/common";
import * as admin from "firebase-admin";
import {
  MultipleDeviceNotificationDto,
  NotificationDto,
  TopicNotificationDto,
} from "./dto/notification.dto";

@Injectable()
export class AppService {
  async sendNotification({ token, title, body, icon }: NotificationDto) {
    try {
      const response = await admin.messaging().send({
        token,
        webpush: {
          notification: {
            title,
            body,
            icon,
          },
        },
      });
      return response;
    } catch (error) {
      throw error;
    }
  }

  async sendNotificationToMultipleTokens({
    tokens,
    title,
    body,
    icon,
  }: MultipleDeviceNotificationDto) {
    const message = {
      notification: {
        title,
        body,
        icon,
      },
      tokens,
    };

    try {
      const response = await admin.messaging().sendMulticast(message);
      console.log("Successfully sent messages:", response);
      return {
        success: true,
        message: `Successfully sent ${response.successCount} messages; ${response.failureCount} failed.`,
      };
    } catch (error) {
      console.log("Error sending messages:", error);
      return { success: false, message: "Failed to send notifications" };
    }
  }

  async sendTopicNotification({
    topic,
    title,
    body,
    icon,
  }: TopicNotificationDto) {
    const message = {
      notification: {
        title,
        body,
        icon,
      },
      topic,
    };

    try {
      const response = await admin.messaging().send(message);
      console.log("Successfully sent message:", response);
      return { success: true, message: "Topic notification sent successfully" };
    } catch (error) {
      console.log("Error sending message:", error);
      return { success: false, message: "Failed to send topic notification" };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This service contains the core logic for sending push notifications. Let's break down each method:

  1. sendNotification:

    • Purpose: Sends a notification to a single device token.
    • Parameters: Takes a NotificationDto object with token, title, body, and icon.
    • Process: Uses Firebase Admin SDK to send a message to the specified token.
  2. sendNotificationToMultipleTokens:

    • Purpose: Sends a notification to multiple device tokens.
    • Parameters: Takes a MultipleDeviceNotificationDto object with tokens array, title, body, and icon.
    • Process: Uses Firebase Admin SDK's sendMulticast method to send to multiple tokens at once.
  3. sendTopicNotification:

    • Purpose: Sends a notification to all devices subscribed to a specific topic.
    • Parameters: Takes a TopicNotificationDto object with topic, title, body, and icon.
    • Process: Uses Firebase Admin SDK to send a message to the specified topic.

Each method constructs a message object with the notification details and uses the Firebase Admin SDK to send the message. The methods return an object indicating the success or failure of the operation.

App Controller

// src/app.controller.ts

import { Controller, Post, Body } from "@nestjs/common";
import { AppService } from "./app.service";
import { ApiTags, ApiOperation, ApiResponse } from "@nestjs/swagger";
import {
  MultipleDeviceNotificationDto,
  TopicNotificationDto,
} from "./dto/notification.dto";

@ApiTags("notifications")
@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post("send-notification")
  @ApiOperation({ summary: "Send a push notification to a single device" })
  @ApiResponse({ status: 200, description: "Notification sent successfully" })
  async sendNotification(
    @Body() body: { token: string; title: string; body: string; icon: string }
  ) {
    return this.appService.sendNotification({
      token: body.token,
      title: body.title,
      body: body.body,
      icon: body.icon,
    });
  }

  @Post("send-multiple-notifications")
  @ApiOperation({ summary: "Send push notifications to multiple devices" })
  @ApiResponse({ status: 200, description: "Notifications sent successfully" })
  async sendMultipleNotifications(@Body() body: MultipleDeviceNotificationDto) {
    return this.appService.sendNotificationToMultipleTokens({
      tokens: body.tokens,
      title: body.title,
      body: body.body,
      icon: body.icon,
    });
  }

  @Post("send-topic-notification")
  @ApiOperation({ summary: "Send a push notification to a topic" })
  @ApiResponse({
    status: 200,
    description: "Topic notification sent successfully",
  })
  async sendTopicNotification(@Body() body: TopicNotificationDto) {
    return this.appService.sendTopicNotification({
      topic: body.topic,
      title: body.title,
      body: body.body,
      icon: body.icon,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

This controller sets up three endpoints:

  1. /send-notification:

    • HTTP Method: POST
    • Purpose: Sends a notification to a single device
    • Body: Expects token, title, body, and icon
  2. /send-multiple-notifications:

    • HTTP Method: POST
    • Purpose: Sends notifications to multiple devices
    • Body: Expects an array of tokens, title, body, and icon
  3. /send-topic-notification:

    • HTTP Method: POST
    • Purpose: Sends a notification to all devices subscribed to a topic
    • Body: Expects topic, title, body, and icon

Each endpoint is decorated with Swagger annotations for clear API documentation. The @ApiTags, @ApiOperation, and @ApiResponse decorators provide metadata for the Swagger UI.

Notification DTO

// src/dto/notification.dto.ts

import { ApiProperty, ApiPropertyOptional, PickType } from "@nestjs/swagger";

export class NotificationDto {
  @ApiProperty({
    type: String,
    description: "Client device token",
  })
  token: string;

  @ApiProperty({
    type: String,
    description: "Notification Title",
  })
  title: string;

  @ApiProperty({
    type: String,
    description: "Notification Body",
  })
  body: string;

  @ApiPropertyOptional({
    type: String,
    description: "Notification Icon / Logo",
  })
  icon: string;
}

export class MultipleDeviceNotificationDto extends PickType(NotificationDto, [
  "title",
  "body",
  "icon",
]) {
  @ApiProperty({
    type: String,
    description: "Clients device token",
  })
  tokens: string[];
}

export class TopicNotificationDto extends PickType(NotificationDto, [
  "title",
  "body",
  "icon",
]) {
  @ApiProperty({
    type: String,
    description: "Subscription topic to send to",
  })
  topic: string;
}
Enter fullscreen mode Exit fullscreen mode

This file defines our Data Transfer Objects (DTOs):

  1. NotificationDto: Base DTO for single device notifications
  2. MultipleDeviceNotificationDto: Extends NotificationDto for multiple device notifications
  3. TopicNotificationDto: Extends NotificationDto for topic notifications

The @ApiProperty and @ApiPropertyOptional decorators provide metadata for Swagger documentation.

Testing the Server

With this setup, you can now run your NestJS server:

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

You can use the Swagger UI at http://localhost:9000/api/docs to test your endpoints. To send a notification to your React app:

  1. Get the FCM token from your React app (as implemented in the previous article)
  2. Use the /send-notification endpoint, providing the token, title, body and icon of the notification

Connecting with the React Frontend

To use this NestJS server with the React frontend we built in the previous article:

  1. When your React app receives an FCM token, send it to this NestJS server and store it (you might want to create an endpoint for this).
  2. Use the stored token(s) when calling the notification endpoints.

Next Steps

  1. Implement token storage in a database for persistent device targeting.
  2. Add authentication to your NestJS server to secure the notification endpoints.
  3. Implement more advanced notification features like actions, images, etc.

Conclusion

This NestJS server provides a robust backend for sending push notifications to your web application. By extending it with multiple token support and topic notifications, you now have a scalable solution for reaching your users with timely updates and information.


Stay Updated and Connected

To ensure you don't miss any part of this series and to connect with me for more in-depth discussions on Software Development (Web, Server, Mobile or Scraping / Automation), push notifications, and other exciting tech topics, follow me on:


Your engagement and feedback drive this series forward. I'm excited to continue this journey with you and help you master the art of push notifications across web and mobile platforms.
Don't hesitate to reach out with questions, suggestions, or your own experiences with push notifications.
You can find below the working source code for this article.
Source Code ๐Ÿš€

Top comments (0)