Building Web Push Notification Server with Nest Js and FCM

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

Introduction

Project Setup

Installing NestJS CLI
Creating a New NestJS Project
Installing Dependencies

Project Structure

Implementing the Server

Main Application File (src/main.ts)
App Module with Firebase Configuration (src/app.module.ts)
App Service (src/app.service.ts)
App Controller (src/app.controller.ts)
Notification DTO (src/dto/notification.dto.ts)

Testing the Server
Connecting with the React Frontend
Next Steps
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

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

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

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:

src/main.ts: The entry point of our application

src/app.module.ts: The root module of our application

src/app.service.ts: The service containing our push notification logic

src/app.controller.ts: The controller containing our push notification endpoints

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();

Let’s break down this file:

We import necessary modules from NestJS and Swagger.
The bootstrap function is the entry point of our application.
We create a NestJS application instance with NestFactory.create(AppModule).
We set up Swagger for API documentation using DocumentBuilder.
We create a Swagger document and set it up at the “/api/docs” endpoint.
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),
}),
});
}
}

This file sets up our main application module:

We import necessary modules and services.
The config() call loads environment variables from a .env file.
We define our AppModule with its controllers and providers.
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 };
}
}
}

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

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.

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.

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,
});
}
}

This controller sets up three endpoints:

/send-notification:

HTTP Method: POST
Purpose: Sends a notification to a single device
Body: Expects token, title, body, and icon

/send-multiple-notifications:

HTTP Method: POST
Purpose: Sends notifications to multiple devices
Body: Expects an array of tokens, title, body, and icon

/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;
}

This file defines our Data Transfer Objects (DTOs):

NotificationDto: Base DTO for single device notifications

MultipleDeviceNotificationDto: Extends NotificationDto for multiple device notifications

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

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

Get the FCM token from your React app (as implemented in the previous article)
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:

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).
Use the stored token(s) when calling the notification endpoints.

Next Steps

Implement token storage in a database for persistent device targeting.
Add authentication to your NestJS server to secure the notification endpoints.
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:

GitHub
Linkedin
X (Twitter)

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 🚀

Please follow and like us:
Pin Share