Full-stack app tutorial with NestJS and React
Tags: nestjs, react, mongodb
Published: 2022-02-14 15:22:10

**Written by [Clara Ekekenta](https://blog.logrocket.com/author/claraekekenta/)✏️**

NestJS is a robust framework for building efficient, scalable Node.js server-side applications. Nest offers many features that allow developers to build web apps using their programming paradigms of choice (functional, object-oriented, or functional reactive). Nest also uses robust Node.js frameworks, like [Express](https://expressjs.com) (its default) and [Fastify](https://www.fastify.io), and includes inbuilt support for Typescript, with the freedom to use pure JavaScript.

This tutorial will illustrate the combined power of NestJS and React by using both to build a full-stack video streaming application.

Why video streaming? Well, streaming media is one of the most common use cases for data streaming. In the scenario of a video app, streaming enables a user to watch a video immediately without first downloading the video. Streaming saves the user time and does not consume storage space.

Streaming is advantageous for app performance, as well. With this type of data transmission, data is sent in small segments or chunks, rather than all at once. This is beneficial for app efficiency and cost management.

In this article, we’ll take a deep dive into building the app backend with Nest.js, building the app frontend with React, and then deploying the full-stack app.

## Getting started

This hands-on tutorial has the following prerequisites:

*   [Node.js](https://nodejs.org/) version >= 10.13.0 installed, except for version 13
*   MongoDB database
*   Ubuntu 20.04, or the OS of your choosing

## Building the Nest.js backend

To create the app’s backend, we’ll follow these steps:

*   [Install and configure the Nest.js project](#installing-and-configuring-nestjs)
*   [Install the dependencies](#installing-the-dependencies)
*   [Set up the Nest server](#setting-up-the-nest-server)
*   [Set up the MongoDB database](#setting-up-the-mongodb-database)
*   [Define the schema](#defining-the-schema)
*   [Define the application routes](#defining-the-application-routes)
*   [Create user authentication](#creating-user-authentication)
*   [Create the video controller](#creating-the-video-controller)
*   [Create the video service](#creating-the-video-service)
*   [Create the middleware](#creating-the-middleware)

### Installing and configuring Nest.js

To install and configure a new Nest.js project, we’ll use Nest’s command-line interface.

Open the terminal and run the following command:

```shell
npm i -g @nestjs/cli
```

Once the installation is complete, create a project folder:

```shell
mkdir VideoStreamApp && cd VideoStreamApp
```

Next, create the new Nest.js project by running this command:

```shell
nest new backend
```

When prompted to choose a package manager for the project, select npm.

This will create a `backend` folder, node modules, and a few other boilerplate files. An `src` folder will also be created and populated with several core files. You can read more about the files in the [NestJS official documentation](https://docs.nestjs.com/first-steps).

Nest, let’s cd into the backend directory:

```shell
cd backend
```

### Installing the dependencies

Next, let’s install the dependencies we’ll need for this project:

*   [Mongoose](https://mongoosejs.com): Node.js-based ODM library for MongoDB
*   [Multer](https://www.npmjs.com/package/multer): Middleware for handling file uploads
*   [JSON web token](https://jwt.io/introduction) (JWT): Authentication handler
*   Universality unique ID (UUID): Random file name generator

Now, run the following code:

```shell
npm i -D @types/multer @nestjs/mongoose mongoose @nestjs/jwt passport-jwt @types/bcrypt bcrypt @types/uuid @nestjs/serve-static
```

Once the installation of the dependencies is complete, we’ll set up a Nest server for the project.

### Setting up the Nest server

Now that we’ve installed the dependencies, let’s set up the Nest server by creating additional folders in the `src` directory. We’ll create a `model`, `controller` `service`, and `utils` directories in the `src` directory.

Next, open the `src/main.ts` file and enable the [Cors](https://www.npmjs.com/package/cors) connect/express npm package by adding the following snippet to the Boostrap function:

```js
 app.enableCors();
```

### Setting up the MongoDB database

We’ll use [Mongoose](https://mongoosejs.com/docs/api.html) to connect the application to the MongoDB database.

First, we’ll set up a MongoDB database for the application. Open the `/src/app.module.ts` file, and add the following snippet:

```js
...
import { MongooseModule } from '@nestjs/mongoose';
@Module({
  imports: [
     MongooseModule.forRoot('mongodb://localhost:27017/Stream'),
  ],
...
```

In this code, we import the `MongooseModule` into the root `AppModule`and use the `forRoot` method to configure the database.

### Defining the schema

Now that the application has been connected to the MongoDB database, let’s define the database schema that will be required by the application. Open the `/src/model` folder, create a `user.schema.ts` file, and add the following snippet:

```js
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
export type UserDocument = User & Document;
@Schema()
export class User {
    @Prop({required:true})
    fullname: string;
    @Prop({required:true, unique:true, lowercase:true})
    email: string;
    @Prop({required:true})
    password: string
    @Prop({default: Date.now() })
    createdDate: Date
}
export const UserSchema = SchemaFactory.createForClass(User)
```

In this code, we import the `@Prop()`, `@Schema()`, `@SchemaFactory()` decorators from Mongoose. The `@Prop()` decorator will be used to define the properties of the database collections. The `@Schema()` decorator will mark a class for the schema definition, and the `@SchemaFactory()` decorator will generate the schema.

We also define some validity rules in the prop decorator. We expect all fields to be required. We specify that `email` should be unique and converted to lowercase. We also specify that the current date should be used for the `createdDate` field’s default date.

Next, let’s create a `video.schema.ts` file in the `model` directory and add the following snippet:

```js
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import * as mongoose from "mongoose";
import { User } from "./user.model";

export type VideoDocument = Video & Document;
@Schema()
export class Video {
    @Prop()
    title: string;
    @Prop()
    video: string;
    @Prop()
    coverImage: string;
    @Prop({ default: Date.now() })
    uploadDate: Date
    @Prop({ type: mongoose.Schema.Types.ObjectId, ref: "User" })
    createdBy: User
}
export const VideoSchema = SchemaFactory.createForClass(Video)
```

In this code, we import `mongoose` and the `User` schema class. This will enable us to reference and save the details about users who create videos with the app.

### Defining the application routes

Now that the schema has been defined, it’s time to define the application’s routes. Let’s start by creating a `user.controller.ts` file in the `controllers` directory.

Next, we’ll import the decorators needed for the user route, import the `User` schema class, `UserService` class (which we’ll create a little later in this article), and the `JwtService` class to handle user authentication:

```js
import { Body, Controller, Delete, Get, HttpStatus, Param, Post, UploadedFiles, Put, Req, Res } from "@nestjs/common";
import { User } from "../model/user.schema";
import { UserService } from "../model/user.service";
import { JwtService } from '@nestjs/jwt'
...
```

We’ll use the `@Controller()` decorator to create the `Signup` and `Signin` routes, passing the `api` URL. We’ll also create a `UserController` class with a `constructor` function where we’ll create variables for the `userService` class and the `JwtService` class.

```js
@Controller('/api/v1/user')
export class UserController {
    constructor(private readonly userServerice: UserService,
        private jwtService: JwtService
    ) { }
...
```

Now, we’ll use the `@Post` decorator to create the `Signup` and `Signin` routes, both of which will listen for a `Post` request:

```js
@Post('/signup')
    async Signup(@Res() response, @Body() user: User) {
        const newUSer = await this.userServerice.signup(user);
        return response.status(HttpStatus.CREATED).json({
            newUSer
        })
    }
    @Post('/signin')
    async SignIn(@Res() response, @Body() user: User) {
        const token = await this.userServerice.signin(user, this.jwtService);
        return response.status(HttpStatus.OK).json(token)
    }
}
```

In this code, we use the `@Res()` decorator to send a response to the client, and the `@Body()` decorator to parse the data in the request body of the `Signup` route.

We create a new user by sending the `user` Schema object to the `userSevervice` signup method and then return the new user to the client with a 201 status code using the inbuilt Nest `HttpsStatus.CREATED` method.

We send the `user` schema object and the `jwtService` as parameters for the `Signin` routes. Then, we invoke the `Signin` method in the `userService` to authenticate the `user` and return a `token` to the client if the sign-in is successful.

### Creating user authentication

Now we’ll create the app’s security and user identity management. This includes all initial interactions a user will have with the app, such as sign-in, authentication, and password protection.

First, open the `/src/app.module.ts` file and import `jwtService`and `ServeStaticModule` into the root `AppModule`. The `ServeStaticModule` decorator enables us to render the files to the client.

Next, we’ll create the `constants.ts` file in the `utils` directory and export the JWT `secret` using the following snippet:

```js
export const secret = 's038-pwpppwpeok-dffMjfjriru44030423-edmmfvnvdmjrp4l4k';
```

On production, the `secret` key should be securely stored in an .env file or put in a dedicated secret manager. The app module should look similar to the following snippet:

```js
...
import { ServeStaticModule } from '@nestjs/serve-static';
import { JwtModule } from '@nestjs/jwt';
import { secret } from './utils/constants';
import { join } from 'path/posix';

@Module({
  imports: [
    ....
    JwtModule.register({
      secret,
      signOptions: { expiresIn: '2h' },
    }),
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', 'public'),
    }),
   ...
  ],
...
```

Next, we'll create a `user.service.ts` file in the service folder, and add the following snippet:

```js
import { Injectable, HttpException, HttpStatus } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { User, UserDocument } from "../model/user.schema";
import * as bcrypt from 'bcrypt';
import { JwtService } from '@nestjs/jwt';
...
```

In this code, we import `Injectable`, `HttpException`, `HttpStatus`, `InJectModel`, `Model`, `bcrypt`, and `JwtService`. The `@Injectable()` decorator attaches metadata, declaring that `UserService` is a class that can be managed by the Nest inversion of control (IoC) container. The `@HttpException()` decorator will be used for error handling.

Now, we’ll create the `UserService` class and inject the schema into the `constructor` function using the `@InjectModel` decorator:

    //javascript
    ...
    @Injectable()
    export class UserService {
        constructor(@InjectModel(User.name) private userModel: Model&lt;UserDocument&gt;,
        ) { }
    ...

Next, we’ll create a `signup` function that will return a `user` as a promise. We’ll use `bcrypt` to salt and hash the user’s password for additional security. We’ll save the hashed version of the password to the database and return the newly created user, `newUser`.

```js
...
async signup(user: User): Promise<User> {
        const salt = await bcrypt.genSalt();
        const hash = await bcrypt.hash(user.password, salt);
        const reqBody = {
            fullname: user.fullname,
            email: user.email,
            password: hash
        }
        const newUser = new this.userModel(reqBody);
        return newUser.save();
    }
...
```

The next step is to create a `signin` function that will allow users to log in to the application.

First, we’ll run a query on the `userModel` to determine if the user record already exists in the collection. When a user is found, we’ll use `bcrypt` to compare the entered password to the one stored in the database. If the passwords match, we’ll provide the user with an access token. If the passwords do not match, the code will throw an exception.

```js
...
    async signin(user: User, jwt: JwtService): Promise<any> {
        const foundUser = await this.userModel.findOne({ email: user.email }).exec();
        if (foundUser) {
            const { password } = foundUser;
            if (bcrypt.compare(user.password, password)) {
                const payload = { email: user.email };
                return {
                    token: jwt.sign(payload),
                };
            }
            return new HttpException('Incorrect username or password', HttpStatus.UNAUTHORIZED)
        }
        return new HttpException('Incorrect username or password', HttpStatus.UNAUTHORIZED)
    }
...
```

Next, we create a `getOne` function to retrieve user data based on an `email` address:

```js
  async getOne(email): Promise<User> {
        return await this.userModel.findOne({ email }).exec();
    }
```

### Creating the video controller

Now, we’ll create the video controller. First, we need to configure Multer to permit the uploading and streaming of videos.

Open the `/src/app.module.ts` file and add the following snippet:

```js
...
import { MulterModule } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { v4 as uuidv4 } from 'uuid';

@Module({
 imports: [
    MongooseModule.forRoot('mongodb://localhost:27017/Stream'),
    MulterModule.register({
      storage: diskStorage({
        destination: './public',
        filename: (req, file, cb) => {
          const ext = file.mimetype.split('/')[1];
          cb(null, `${uuidv4()}-${Date.now()}.${ext}`);
        },
      })
    }),
...
```

In this code, we import the `MulterModule` into the root `AppModule`. We import `diskStorage` from Multer, providing full control to store files to disk. We also import `v4` from `uuid` to generate random names for the files we are uploading. We use the `MulterModule.register` method to configure the file upload to disk in a `/public` folder.

Next, we create a `video.conmtroller.ts` file in the controller directory and add the below snippet:

```js
import { Body, Controller, Delete, Get, HttpStatus, Param, Post, UseInterceptors, UploadedFiles, Put, Req, Res, Query } from "@nestjs/common";
import { Video } from "../model/video.schema"
import { VideoService } from "../video.service";
import { FileFieldsInterceptor, FilesInterceptor } from "@nestjs/platform-express";
...
```

In this code, we import `UseInterceptors`, `UploadedFiles`, `Video` schema, `VideoService` class, `FileFieldsInterceptor`, `FilesInterceptor`, and other decorators required for the video route.

Next, we’ll create the video controller using the `@Controller` decorator and pass in the `api` URL. Then, we’ll create a `VideoController` class with a `constructor()` function where we’ll create a `private` variable for the `VideoSevice` class.

```js
@Controller('/api/v1/video')
export class VideoController {
    constructor(private readonly videoService: VideoService){}
...
```

Now, we’ll use the `@UseInterceptors` decorator to bind the `@FileFieldsInterceptor` decorator, which extracts files from the `request` with the `@UploadedFiles()` decorator.

We’ll pass in the file fields to the `@FileFieldsInterceptor` decorator. The `maxCount` property specifies the need for only one file per field.

All of the form data files will be stored in the `files` variable. We’ll create a `requestBody` variable and create objects to hold the form data values.

This variable is passed to the `videoService` class to save the details of the video, while Multer saves the video and `coverImage` to the disk. Once the record is saved, the created video object is returned to the client with a 201 status code.

Next, we’ll create `Get`, `Put`, `Delete` routes to get, update, and delete a video using its ID.

```js
...   
    @Post()
    @UseInterceptors(FileFieldsInterceptor([
        { name: 'video', maxCount: 1 },
        { name: 'cover', maxCount: 1 },
    ]))
    async createBook(@Res() response, @Req() request, @Body() video: Video, @UploadedFiles() files: { video?: Express.Multer.File[], cover?: Express.Multer.File[] }) {
        const requestBody = { createdBy: request.user, title: video.title, video: files.video[0].filename, coverImage: files.cover[0].filename }
        const newVideo = await this.videoService.createVideo(requestBody);
        return response.status(HttpStatus.CREATED).json({
            newVideo
        })
    }
    @Get()
    async read(@Query() id): Promise<Object> {
        return await this.videoService.readVideo(id);
    }
    @Get('/:id')
    async stream(@Param('id') id, @Res() response, @Req() request) {
        return this.videoService.streamVideo(id, response, request);
    }
    @Put('/:id')
    async update(@Res() response, @Param('id') id, @Body() video: Video) {
        const updatedVideo = await this.videoService.update(id, video);
        return response.status(HttpStatus.OK).json(updatedVideo)
    }
    @Delete('/:id')
    async delete(@Res() response, @Param('id') id) {
        await this.videoService.delete(id);
        return response.status(HttpStatus.OK).json({
            user: null
        })
    }
}
```

### Creating the video service

With the video controller created, let’s create the video service. We’ll start by creating a `video.service.ts` file in the service folder. Then, we’ll import the necessary modules using this snippet:

```js
import {
    Injectable,
    NotFoundException,
    ServiceUnavailableException,
} from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { Video, VideoDocument } from "../model/video.schema";
import { createReadStream, statSync } from 'fs';
import { join } from 'path';
import { Request, Response } from 'express';
...
```

In this code, we import `createReadStream` and `statSync` from the `fs` module. We use the `createReadStream` to read files in our file system, and `statSync` to get the file’s details. Then, we import the `Video` model and `VideoDocument`.

Now, we’ll create our `VideoService` class, and inject the schema into the `constructor` function using the `@InjectModel` decorator:

```js
...
@Injectable()
export class VideoService {
    constructor(@InjectModel(Video.name) private videoModel: Model<VideoDocument>) { }
...
```

Next, we’ll use the `createVideo` function to save the video details to the database collection and return the created the `newVideo.save` object:

```js
...
    async createVideo(video: Object): Promise<Video> {
        const newVideo = new this.videoModel(video);
        return newVideo.save();
    }
...
```

Then, we'll create the `readVideo` function to get video details based on the `id` in the request parameter. We’ll `populate` the name of the user who created the video and return this name, `createdBy`, to the client.

```js
...
   async readVideo(id): Promise<any> {
        if (id.id) {
            return this.videoModel.findOne({ _id: id.id }).populate("createdBy").exec();
        }
        return this.videoModel.find().populate("createdBy").exec();
    }
...
```

Next, we’ll create the `streamVideo` function to send a video as a stream to the client. We’ll query the database to get the video’s details according to `id`. If the video `id` is found, we get the initial range value from the request headers. Then we’ll use the video details to get the video from the file system. We’ll break the video into `1mb` chunks and send it to the client. If the video `id` is not found, the code will throw a `NotFoundException` error.

```js
...
   async streamVideo(id: string, response: Response, request: Request) {
        try {
            const data = await this.videoModel.findOne({ _id: id })
            if (!data) {
                throw new NotFoundException(null, 'VideoNotFound')
            }
            const { range } = request.headers;
            if (range) {
                const { video } = data;
                const videoPath = statSync(join(process.cwd(), `./public/${video}`))
                const CHUNK_SIZE = 1 * 1e6;
                const start = Number(range.replace(/\D/g, ''));
                const end = Math.min(start + CHUNK_SIZE, videoPath.size - 1);
                const videoLength = end - start + 1;
                response.status(206)
                response.header({
                    'Content-Range': `bytes ${start}-${end}/${videoPath.size}`,
                    'Accept-Ranges': 'bytes',
                    'Content-length': videoLength,
                    'Content-Type': 'video/mp4',
                })
                const vidoeStream = createReadStream(join(process.cwd(), `./public/${video}`), { start, end });
                vidoeStream.pipe(response);
            } else {
                throw new NotFoundException(null, 'range not found')
            }

        } catch (e) {
            console.error(e)
            throw new ServiceUnavailableException()
        }
    }
...
```

Next, we’ll create `update` and `delete` functions to update or delete videos in the database collection:

```js
...
    async update(id, video: Video): Promise<Video> {
        return await this.videoModel.findByIdAndUpdate(id, video, { new: true })
    }
    async delete(id): Promise<any> {
        return await this.videoModel.findByIdAndRemove(id);
    }
}
```

Although the controllers and services are defined, Nest still doesn't know they exist and as a result, won't create an instance of those classes.

To remedy this, we must add the controllers to the `app.module.ts file`, and add the services to the `providers:` list. Then, we’ll export the schema and models in the `AppModule` and register the `ServeStaticModule`. This enables us to render the files to the client.

```js
....
import { ServeStaticModule } from '@nestjs/serve-static';
import { VideoController } from './controller/video.controller';
import { VideoService } from './service/video.service';
import { UserService } from './service/user.service';
import { UserController } from './controller/user.controller';
import { Video, VideoSchema } from './model/video.schema';
import { User, UserSchema } from './model/user.schema';

@Module({
  imports: [
    ....
    MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
    MongooseModule.forFeature([{ name: Video.name, schema: VideoSchema }]),
    ....
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', 'public'),
    }),
  ],
  controllers: [AppController, VideoController, UserController],
  providers: [AppService, VideoService, UserService],
})
```

### Creating the middleware

At this point, Nest is now aware that the controllers and services in the app exist. The next step is to create middleware to protect the video routes from unauthenticated users.

To get started, let’s create an `app.middleware.ts` file in the `/src` folder, and add the following snippet:

```js
import { JwtService } from '@nestjs/jwt';
import { Injectable, NestMiddleware, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { UserService } from './service/user.service';
interface UserRequest extends Request {
    user: any
}
@Injectable()
export class isAuthenticated implements NestMiddleware {
    constructor(private readonly jwt: JwtService, private readonly userService: UserService) { }
    async use(req: UserRequest, res: Response, next: NextFunction) {
        try{

            if (
                req.headers.authorization &&
                req.headers.authorization.startsWith('Bearer')
            ) {
                const token = req.headers.authorization.split(' ')[1];
                const decoded = await this.jwt.verify(token);
                const user = await this.userService.getOne(decoded.email)
                if (user) {
                    req.user = user
                    next()
                } else {
                    throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED)

                }
            } else {
                throw new HttpException('No token found', HttpStatus.NOT_FOUND)

            }
        }catch {
         throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED)
       }
    }
}
```

In this code, we create an `isAuthenticated` class, which implements the `NestMiddleware`. We get the token from the client in the request headers and verify the token. If the token is valid, the user is granted access to the video routes. if the token is invalid, we raise an `HttpException`.

Next, we’ll open the `app.module.ts` file and configure the middleware. We’ll exclude the stream route since we’re streaming diretory from a video element in the frontend:

```js
import { Module, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(isAuthenticated)
      .exclude(
        { path: 'api/v1/video/:id', method: RequestMethod.GET }
      )
      .forRoutes(VideoController);
  }
}
```

Now, let’s run the following command to start the NestJS server:

```shell
npm run start:dev
```

## Building the React app frontend

To streamline this portion of the tutorial, I’ve created a [GitHub repo](https://github.com/Claradev32/VideoStreamApp/tree/dev) for the UI of the app’s frontend. To get started, clone to the `dev` branch and let’s focus on consuming the API and the application logic.

To set up the frontend of the video streaming React app, we’ll build functionality for the following:

*   [Create the login](#creating-the-login)
*   [Create user accounts](#creating-user-accounts)
*   [Add videos to the app library](#adding-videos-to-the-library)
*   [Display the video list in the app library](#displaying-the-video-list)
*   [Stream the videos](#streaming-the-videos)

### Creating the login

With the UI up and running, let's handle the logic to log users into the app. Open the `Component/Auth/Signin.js` file, and import `axios` and `useNavigation`:

```js
...
import axios from 'axios';
import { useNavigate } from "react-router-dom"
...
```

In this code, we use `axios` to make API requests to the backend. `useNavigation` is used to redirect users after a successful sign-in.

Now, let’s create a `handleSubmit` handler function with the following snippet:

```js
...
export default function SignIn({setIsLoggedIn}) {
  const [errrorMessage, setErrorMessage] = React.useState('')
  let navigate = useNavigate();

  const handleSubmit = async (event) => {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const form = {
      email: formData.get('email'),
      password: formData.get('password')
    };
    const { data } = await axios.post("http://localhost:3002/api/v1/user/signin", form);
    if (data.status === parseInt('401')) {
      setErrorMessage(data.response)
    } else {
      localStorage.setItem('token', data.token);
      setIsLoggedIn(true)
      navigate('/video')
    }
  };
...
```

In this code, we destructure `setIsLoggedIn` from our `props`, create an `errorMessage` state to display error messages to users during sign-in. Then, we use the `formData` API to get user `Formdata` from the text fields and use `axios` to send a `.post` request to the backend.

We check the response `status` to see if the sign-in was successful. With a successful sign-in, we save the token that was sent to the user on the browser’s `localStorage`, reset the `setIsLoggedIn` state to true, and redirect the user to the video page. An unsuccessful sign-in will result in a `401(Unauthorized)` response. In this case, we’ll display the error message to the user.

Next, we’ll add an `onSumit` event to the `form` component and bind the `handleSubmit` handler.

```js
...
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }}>
...
```

If there is an `errorMessage`, we’ll display it to the user:

```js
<Typography component="p" variant="p" color="red">
  {errrorMessage}
</Typography>
```

### Creating user accounts

Now, we’re ready to log users into the application. Let's create a `Signup` component that allows users to create an account. Open the `Component/Auth/Signup.js`, and import `axios` and `useNavigate`:

```js
...
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
...
```

Next, we'll create a `handleSubmit` handler function with the following snippet:

```js
...
export default function SignUp() {
    let navigate = useNavigate();
  const handleSubmit = async (event) => {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const form = {
      fullname : data.get('fname') +' '+ data.get('lname'),
      email: data.get('email'),
      password: data.get('password')
    };
    await axios.post("http://localhost:3002/api/v1/user/signup", form); 
    navigate('/')
  };
...
```

In this code, we destructure `setIsLoggedIn` from the `props` and create an `errorMessage` state to display error messages to users during sign-in. Then, we use the `formData` API to get user input data from the form text fields and send a post request to the backend using `axios`. After sign-in, we redirect the user to the sign-in page.

Next, we’ll add an `onSumit` event to the for component and bind the `handleSubmit` handler we just created.

```js
Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
```

### Adding videos to the library

Now that the user authentication components are created, let’s give users the ability to add videos to the library.

We’ll start by opening the `Component/Navbar/Header.js`, and importing `axios`:

```js
...
import axios from 'axios';
...
```

Next, we’ll destructure the `isLoggedIn` state from the properties and create three `React.useState` variables for the `video`, `cover` image, and `title`.

```js
...
const [videos, setVideos] = React.useState("");
const [cover, setCover] = React.useState("");
const [title, setTitle] = React.useState("")
...
```

Now, we’ll create a `submitForm` handler function. In our `submitForm` function, we’ll prevent the form’s default reload, and we’ll get the form submission information using the `formData` API. To authorize the user to access the video endpoints, we’ll get the user's token from the browser’s localStorage, and [send a `.post` HTTP request with `axios`](https://blog.logrocket.com/how-to-make-http-requests-like-a-pro-with-axios/).

```js
...  
   const submitForm = async (e) => {
        e.preventDefault();
        const formData = new FormData();
        formData.append("title", title);
        formData.append("video", video);
        formData.append("cover", cover);
        const token = localStorage.getItem('token');
        await axios.post("http://localhost:3002/api/v1/video", formData, {
            headers: ({
                Authorization: 'Bearer ' + token
            })
        })
    }
...
```

Next, we’ll bind the `submitForm` handler to an `onSumbit` event, and bind the input state set variable to an `onChange` event. The form component should look like this:

```js
 <Box sx={style}>
    <Typography id="modal-modal-title" variant="h6" component="h2">
    <Box component="form" onSubmit={submitForm} noValidate sx={{ mt: 1 }}>
        <label>Video Title:</label>
        <TextField
           margin="normal"
           required
           fullWidth
           id="title"
           name="title"
           autoFocus
           onChange={(e) => setTitle(e.target.value)}
                                                />
     <label>Select Video:</label>
     <TextField
        margin="normal"
        required
        fullWidth
        id="video"
        name="video"
        autoFocus
        type="file"
        onChange={(e) => setVideos(e.target.files[0])}
    />
    <label>Select Cover Image:</label>
    <TextField
       autoFocus
       margin="normal"
       required
       fullWidth
       name="coverImage"
       type="file"
       id="coverImage"
       onChange={(e) => setCover(e.target.files[0])}
    />
   <Button
      type="submit"
      fullWidth
      variant="contained"
      sx={{ mt: 3, mb: 2 }}
    >
    Upload
   </Button>
</Box>
```

### Displaying the video list

Let’s create a `VideoList` component to display the videos to users. Open the `Component/Video/VideoList.js` file, import `axios`, `useParams`, `useEffect`, and `useNavigate`.

    //javascript
    ...
    import { Link, useNavigate } from 'react-router-dom'
    import axios from 'axios';
    ...

Next, we’ll create a `videos` state to store the videos and a `navigate` object to redirect users to the login page when their token expires:

```js
...
    const [videos, setVideos] = React.useState([])
    const navigate = useNavigate();
...
```

We'll use the `React.useState` to send a get request to the API when the component mounts. We’ll get the user's `token` from `localStorage` and [use`axios` to send it in the request headers to the API](https://blog.logrocket.com/using-axios-set-request-headers/):

```js
... 
React.useEffect(() => {
        async function fetchData() {
            try {
                const token = localStorage.getItem('token');
                const {data} = await axios.get('http://localhost:3002/api/v1/video', {
                    headers: ({
                        Authorization: 'Bearer ' + token
                    })
                });
                setVideos(data)
            } catch {
                setLoggedIn(false);
                navigate('/')
            }
        }
        fetchData();
    }, [navigate, setLoggedIn]);
...
```

Next, we’ll loop through the video list in the `videos` state and display the list to users. We’ll use the link `component` to create a link to the video stream page, parsing the video in the URL.

```js
...
{videos.map((video) => {
    return <Grid item xs={12} md={4} key={video._id}>
        <CardActionArea component="a" href="#">
            <Card sx={{ display: 'flex' }}>
                <CardContent sx={{ flex: 1 }}>
                    <Typography component="h2" variant="h5">
                        <Link to={`/video/${video._id}`} style={{ textDecoration: "none", color: "black" }}>{video.title}</Link>
                    </Typography>
                    <Typography variant="subtitle1" color="text.secondary">
                        {video.uploadDate}
                    </Typography>
                </CardContent>
                <CardMedia
                    component="img"
                    sx={{ width: 160, display: { xs: 'none', sm: 'block' } }}
                    image={`http://127.0.0.1:3002/${video.coverImage}`}
                    alt="alt"
                />
            </Card>
        </CardActionArea>
    </Grid>
})}
...
```

### Streaming the videos

Now, let’s create a component to stream any video that a user selects. Open the `Componenet/Video/Video.js` file and import `useNavigation` and `useParams` and `axios`. We will use `useNavigation` and `useParams` to get the `id` of the video that the user wants to stream.

```js
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
```

We’ll send a `GET` request with `axios` with the `videoId` in the URL parameter and the user’s `token` in the request headers for authorization.

If the token is invalid, we’ll reset the `isLoggedIn` state and redirect the user to the login page.

```js
React.useEffect(() => {
        async function fetchData() {
            try {
                const token = localStorage.getItem('token');
                const {data} = await axios.get(`http://127.0.0.1:3002/api/v1/video?id=${videoId}`, {
                    headers: ({
                        Authorization: 'Bearer ' + token
                    })
                });
                setVideoInfo(data)
            } catch {
                setLoggedIn(false);
                navigate('/')
            }
        }
        fetchData();
}, [videoId, navigate, setLoggedIn]);
```

Now, we’ll display the video details to users, and parse the video URL in the video element to stream the video:

```js
<Container>
    <Grid item xs={12} md={12} marginTop={2}>
        <CardActionArea component="a" href="#">
            <Card sx={{ display: 'flex' }}>
                <CardContent sx={{ flex: 1 }}>
                    <video autoPlay controls width='200'>
                        <source src={`http://localhost:3002/api/v1/video/${videoId}`} type='video/mp4' />
                    </video>
                </CardContent>
            </Card>
        </CardActionArea>
    </Grid>
    <Grid container spacing={2} marginTop={2}>
        <Grid item xs={12} md={6}>
            <Typography variant="subtitle1" color="primary">
                Created by:{videoInfo.createdBy?.fullname}
            </Typography>
        </Grid>
        <Grid item xs={12} md={6}>
            <Typography variant="subtitle1" color="primary">
                Created: {videoInfo.uploadDate}
            </Typography>
        </Grid>
        <Grid item xs={12} md={12}>
            <Typography variant="h5">
                {videoInfo.title}
            </Typography>
        </Grid>
    </Grid>
</Container>
```

### Deploying the app

Now, ensuring we are in the `frontend` directory, let’s run the below command to deploy the app:

```shell
npm start 
```

## Conclusion

In this tutorial, we introduced NestJS as a framework for building scalable Node.js applications. We demonstrated this concept by building a full-stack video streaming application using NestJS and React. The code shared in this tutorial may be extended by adding more styling to the UI and also by adding more components.

The full project code used in this article is available on [GitHub](https://github.com/Claradev32/VideoStreamApp/tree/master). Feel free to deploy this app on Heroku and share it with friends.

---

## Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, [try LogRocket](https://www2.logrocket.com/react-performance-monitoring). 

![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)

[LogRocket](https://www2.logrocket.com/react-performance-monitoring) is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — [start monitoring for free](https://www2.logrocket.com/react-performance-monitoring).