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 (its default) and Fastify, 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 NestJS, building the app frontend with React, and then deploying the full-stack app.
This hands-on tutorial has the following prerequisites:
To create the app’s backend, we’ll follow these steps:
To install and configure a new NestJS project, we’ll use Nest’s command-line interface.
Open the terminal and run the following command:
npm i -g @nestjs/cli
Once the installation is complete, create a project folder:
mkdir VideoStreamApp && cd VideoStreamApp
Next, create the new NestJS project by running this command:
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.
Nest, let’s cd
into the backend directory:
cd backend
Next, let’s install the dependencies we’ll need for this project:
Now, run the following code:
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.
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 Connect/Express npm package by adding the following snippet to the Boostrap function:
app.enableCors();
We’ll use Mongoose 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:
... 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.
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:
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:
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.
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:
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.
@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:
@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 userService
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.
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:
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:
... 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:
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:
... @Injectable() export class UserService { constructor(@InjectModel(User.name) private userModel: Model<UserDocument>, ) { } ...
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
.
... 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.
... 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:
async getOne(email): Promise<User> { return await this.userModel.findOne({ email }).exec(); }
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:
... 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:
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.
@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.
... @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 }) } }
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:
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:
... @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:
... 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.
... 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.
... 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:
... 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.
.... 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], })
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:
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:
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:
npm run start:dev
To streamline this portion of the tutorial, I’ve created a GitHub repo 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:
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
:
... 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:
... 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 onSubmit
event to the form
component and bind the handleSubmit
handler.
... <Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }}> ...
If there is an errorMessage
, we’ll display it to the user:
<Typography component="p" variant="p" color="red"> {errrorMessage} </Typography>
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
:
... import { useNavigate } from 'react-router-dom'; import axios from 'axios'; ...
Next, we’ll create a handleSubmit
handler function with the following snippet:
... 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 onSubmit
event to the for component and bind the handleSubmit
handler we just created.
Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
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
:
... 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
.
... 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
.
... 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 onSubmit
event, and bind the input state set variable to an onChange
event. The form component should look like this:
<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>
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
.
... 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:
... 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 useaxios
to send it in the request headers to the API:
... 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.
... {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> })} ...
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.
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.
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:
<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>
Now, ensuring we are in the frontend
directory, let’s run the below command to deploy the app:
npm start
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. Feel free to deploy this app on Heroku and share it with friends.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowValidating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.
Learn how Remix enhances SSR performance, simplifies data fetching, and improves SEO compared to client-heavy React apps.
One Reply to "Full-stack app tutorial with NestJS and React"
Thanks for this nice article. I have gone through it completely and wrote he backend code as I read throgh this. Frontend code I took from git. Except few typo errors (in backend), did not find major errors. One package path/posix I had to change as path-posix.
At the end I realized I can run either frontend or backend. Either you could have given steps how to run both frontend and backend simultaneously or write a separate article in generic way and given a link to follow it. This woud have been helpful for a newbie like me.