Este tutorial ilustrará la potencia combinada de NestJS y React utilizando ambos para construir una aplicación de streaming de vídeo completa.
Pequeña intro
NestJS es un framework robusto para construir aplicaciones del lado del servidor de Node.js eficientes y escalables. Nest ofrece muchas características que permiten a los desarrolladores construir aplicaciones web utilizando los paradigmas de programación que prefieran (funcional, orientado a objetos o funcional reactivo). Nest también utiliza marcos robustos de Node.js, como Express (su opción por defecto) y Fastify, e incluye soporte incorporado para Typescript, con la libertad de utilizar JavaScript puro.
¿Por qué un streaming de vídeo? Bueno, el streaming de medios es uno de los casos de uso más comunes para la transmisión de datos. En el caso de una aplicación de vídeo, el streaming permite al usuario ver un vídeo inmediatamente sin tener que descargarlo primero, además de ahorrar tiempo al usuario y no consume espacio de almacenamiento.
También es ventajoso para el rendimiento de la aplicación. Con este tipo de transmisión de datos, los datos se envían en pequeños segmentos o trozos, en lugar de todos a la vez. Esto es beneficioso para la eficiencia de la aplicación y la gestión de los costes.
En este artículo, nos adentraremos en la construcción del backend de la aplicación con Nest.js, en la construcción del frontend de la aplicación con React y en el despliegue de la aplicación full stack.
Para empezar
Este tutorial práctico tiene los siguientes requisitos previos:
- Node.js version >= 10.13.0 instalado, excepto la versión 13
- Base de datos MongoDB
- Ubuntu 20.04, o el sistema operativo de tu elección
Pasos a seguir
Instalación y configuración de Nest.js
Para instalar y configurar un nuevo proyecto Nest.js, utilizaremos la interfaz de línea de comandos de Nest.
Abre el terminal y ejecuta el siguiente comando:
npm i -g @nestjs/cli
Una vez completada la instalación, crea una carpeta de proyecto:
mkdir VideoStreamApp && cd VideoStreamApp
A continuación, crea el nuevo proyecto Nest.js ejecutando este comando:
nest new backend
Cuando se te pida que elijas un gestor de paquetes para el proyecto, selecciona npm.
Esto creará una carpeta backend
, módulos node y algunos archivos boilerplate. También se creará una carpeta src
y se llenará con varios archivos del núcleo. Puedes leer más sobre los archivos en la documentación oficial de NestJS.
Ahora, vamos a entrar en el directorio del backend:
cd backend
Instalar las dependencias
A continuación, vamos a instalar las dependencias que necesitaremos para este proyecto:
- Mongoose: Biblioteca ODM basada en Node.js para MongoDB
- Multer: Middleware para gestionar la carga de archivos
- JSON web token (JWT): Gestor de autenticación
- ID único universal (UUID): Generador de nombres de archivo aleatorios
Ahora, ejecuta el siguiente código:
npm i -D @types/multer @nestjs/mongoose mongoose @nestjs/jwt passport-jwt @types/bcrypt bcrypt @types/uuid @nestjs/serve-static
Una vez completada la instalación de las dependencias, configuraremos un servidor Nest para el proyecto.
Configurar el servidor Nest
Ahora que hemos instalado las dependencias, vamos a configurar el servidor Nest creando carpetas adicionales en el directorio src
. Crearemos los directorios model
, controller
, service
y utils
en src
.
A continuación, abre el archivo src/main.ts
y activa el paquete npm Cors connect/express añadiendo el siguiente fragmento a la función Bootstrap:
app.enableCors();
Configurar la base de datos MongoDB
Utilizaremos Mongoose para conectar la aplicación a la base de datos MongoDB.
En primer lugar, configuraremos una base de datos MongoDB para la aplicación. Abre el archivo /src/app.module.ts
y añade el siguiente fragmento:
...
import { MongooseModule } from '@nestjs/mongoose';
@Module({
imports: [
MongooseModule.forRoot('mongodb://localhost:27017/Stream'),
],
...
En este código, importamos el MongooseModule
en el AppModule
y utilizamos el método forRoot
para configurar la base de datos.
Definir el schema
Ahora que la aplicación se ha conectado a la base de datos MongoDB, vamos a definir el esquema de la base de datos que necesitará la aplicación. Abre la carpeta /src/model
, crea un archivo user.schema.ts
y añade el siguiente fragmento:
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)
En este código, importamos los decoradores @Prop()
, @Schema()
, @SchemaFactory()
de Mongoose.
@Prop()
se utilizará para definir las propiedades de las colecciones de la base de datos.@Schema()
marcará una clase para la definición del esquema.@SchemaFactory()
generará el esquema.
También definimos algunas reglas de validez en el decorador prop. Esperamos que todos los campos sean obligatorios. Especificamos que el correo electrónico debe ser único y convertido a minúsculas. También especificamos que la fecha actual debe usarse para la fecha por defecto del campo createdDate
.
A continuación, vamos a crear un archivo video.schema.ts
en el directorio model
y añadir el siguiente fragmento:
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)
En este código, importamos mongoose
y la clase User
schema. Esto nos permitirá referenciar y guardar los detalles de los usuarios que crean vídeos con la aplicación.
Definir las rutas de aplicación
Ahora que el esquema ha sido definido, es el momento de definir las rutas de la aplicación. Empecemos por crear un archivo user.controller.ts
en el directorio de controllers
.
A continuación, importaremos los decoradores necesarios para la ruta de usuario, importaremos la clase User
schema, la clase UserService
(que crearemos un poco más adelante en este artículo), y la clase JwtService
para manejar la autenticación del usuario:
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'
...
Utilizaremos el decorador @Controller()
para crear las rutas Signup
y Signin
, pasando la URL de la api. También crearemos una clase UserController
con una función constructora donde crearemos variables para la clase userService
y la clase JwtService
.
@Controller('/api/v1/user')
export class UserController {
constructor(private readonly userServerice: UserService,
private jwtService: JwtService
) { }
...
Ahora, usaremos el decorador @Post
para crear las rutas Signup
y Signin
, las cuales escucharán una petición Post
:
@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)
}
}
En este código, utilizamos el decorador @Res() para enviar una respuesta al cliente, y el decorador @Body() para analizar los datos en el cuerpo de la solicitud de la ruta Signup.
Creamos un nuevo usuario enviando el user
objeto Schema al userSevervice
método de registro y luego devolvemos el nuevo usuario al cliente con un código de estado 201 utilizando el método incorporado Nest HttpsStatus.CREATED
Enviamos el user
objeto de esquema y el jwtService
como parámetros para las rutas Signin
. Luego, invocamos el método Signin
en el userService
para autenticar al usuario
y devolver un token
al cliente si el inicio de sesión es exitoso.
Crear la autentificación del usuario
Ahora crearemos la seguridad de la aplicación y la gestión de la identidad del usuario. Esto incluye todas las interacciones iniciales que un usuario tendrá con la aplicación, como el inicio de sesión, la autenticación y la protección de la contraseña.
En primer lugar, abre el archivo /src/app.module.ts
e importa jwtService
y ServeStaticModule
en el AppModule
. El decorador ServeStaticModule
nos permite renderizar los archivos al cliente.
A continuación, crearemos el archivo constants.ts
en el directorio utils
y exportaremos la constante secret
de JWT con el siguiente fragmento:
export const secret = 's038-pwpppwpeok-dffMjfjriru44030423-edmmfvnvdmjrp4l4k';
En producción, la clave secreta debe almacenarse de forma segura en un archivo .env o colocarse en un gestor de claves dedicado. El módulo de la aplicación debe tener un aspecto similar al siguiente fragmento:
...
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'),
}),
...
],
...
A continuación, crearemos un archivo user.service.ts
en la carpeta service, y añadiremos el siguiente fragmento:
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';
...
En este código, importamos Injectable
, HttpException
, HttpStatus
, InJectModel
, Model
, bcrypt
y JwtService
.
El decorador @Injectable()
adjunta metadatos, declarando que UserService
es una clase que puede ser gestionada por el contenedor de inversión de control (IoC) Nest. El decorador @HttpException()
se utilizará para la gestión de errores.
Ahora, crearemos la clase UserService
e inyectaremos el esquema en la función constructora utilizando el decorador @InjectModel
:
//javascript
...
@Injectable()
export class UserService {
constructor(@InjectModel(User.name) private userModel: Model<UserDocument>,
) { }
...
A continuación, crearemos una función de registro que devolverá un usuario como promesa. Utilizaremos bcrypt
para aplicar salt y hash a la contraseña del usuario para mayor seguridad. Guardaremos la versión hash de la contraseña en la base de datos y devolveremos el usuario recién creado, 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();
}
...
El siguiente paso es crear una función de inicio de sesión signin
que permita a los usuarios conectarse a la aplicación.
En primer lugar, ejecutaremos una consulta en el userModel
para determinar si el registro de usuario ya existe en la colección. Cuando se encuentre un usuario, utilizaremos bcrypt
para comparar la contraseña introducida con la almacenada en la base de datos. Si las contraseñas coinciden, proporcionaremos al usuario un token de acceso. Si las contraseñas no coinciden, el código lanzará una excepción.
...
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)
}
...
A continuación, creamos una función getOne
para recuperar los datos del usuario a partir de una dirección de correo electrónico:
async getOne(email): Promise<User> {
return await this.userModel.findOne({ email }).exec();
}
Crear el controlador de vídeo
Ahora, crearemos el controlador de vídeo. En primer lugar, tenemos que configurar Multer para que permita la subida y la transmisión de vídeos.
Abre el archivo /src/app.module.ts
y añade el siguiente fragmento:
...
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}`);
},
})
}),
...
En este código, importamos el MulterModule
en el AppModule
. Importamos diskStorage
de Multer, que proporciona un control total para almacenar los archivos en el disco. También importamos v4
de uuid
para generar nombres aleatorios para los archivos que estamos subiendo. Utilizamos el método MulterModule.register
para configurar la subida de archivos al disco en una carpeta /public
.
A continuación, creamos un archivo video.controller.ts
en el directorio del controlador y añadimos el siguiente fragmento:
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";
...
En este código, importamos UseInterceptors
, UploadedFiles
, Video
schema, la clase VideoService
, FileFieldsInterceptor
, FilesInterceptor
, y otros decoradores necesarios para la ruta de vídeo.
A continuación, crearemos el controlador de vídeo utilizando el decorador @Controller
y pasaremos la URL de la api
. A continuación, crearemos una clase VideoController
con una función constructor()
en la que crearemos una variable privada para la clase VideoSevice
.
@Controller('/api/v1/video')
export class VideoController {
constructor(private readonly videoService: VideoService){}
...
Ahora, utilizaremos el decorador @UseInterceptors
para vincular el decorador @FileFieldsInterceptor
, que extrae los archivos de la solicitud con el decorador @UploadedFiles()
.
Pasaremos los campos de archivo al decorador @FileFieldsInterceptor
. La propiedad maxCount
especifica la necesidad de un solo archivo por campo.
Todos los archivos de datos del formulario se almacenarán en la variable files
. Crearemos una variable requestBody
y crearemos objetos para guardar los valores de los datos del formulario.
Esta variable se pasa a la clase videoService
para que guarde los detalles del vídeo, mientras Multer guarda el vídeo y coverImage
en el disco. Una vez guardado el registro, el objeto vídeo creado se devuelve al cliente con un código de estado 201.
A continuación, crearemos las rutas Get
, Put
, Delete
para obtener, actualizar y eliminar un vídeo utilizando su 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
})
}
}
Creación del servicio de vídeo
Con el controlador de vídeo creado, vamos a crear el servicio de vídeo. Empezaremos creando un archivo video.service.ts
en la carpeta del servicio. A continuación, importaremos los módulos necesarios utilizando este fragmento:
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';
...
En este código, importamos createReadStream
y statSync
del módulo fs
. Utilizamos el createReadStream
para leer los archivos en nuestro sistema de archivos, y el statSync
para obtener los detalles del archivo. A continuación, importamos el modelo Video
y VideoDocument
.
Ahora, crearemos nuestra clase VideoService
, e inyectaremos el esquema en la función constructora utilizando el decorador @InjectModel
:
...
@Injectable()
export class VideoService {
constructor(@InjectModel(Video.name) private videoModel: Model<VideoDocument>) { }
...
A continuación, utilizaremos la función createVideo
para guardar los detalles del vídeo en la colección de la base de datos y devolver el objeto newVideo.save
creado:
...
async createVideo(video: Object): Promise<Video> {
const newVideo = new this.videoModel(video);
return newVideo.save();
}
...
Ahora, crearemos la función readVideo
para obtener los detalles del vídeo en función del id del parámetro de solicitud. Rellenamos el nombre del usuario que creó el vídeo y devolveremos este nombre, createdBy
, al cliente.
...
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();
}
...
A continuación, crearemos la función streamVideo
para enviar un vídeo como flujo al cliente. Consultaremos la base de datos para obtener los detalles del vídeo según el id. Si se encuentra el id
del vídeo, obtendremos el valor del rango inicial de las cabeceras de la petición. Luego utilizaremos los detalles del vídeo para obtener el vídeo del sistema de archivos. Dividiremos el vídeo en trozos de 1 mb
y lo enviaremos al cliente. Si no se encuentra el identificador del vídeo, el código lanzará un error NotFoundException
.
...
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()
}
}
...
A continuación, crearemos funciones update
y delete
para actualizar o eliminar vídeos en la colección de la base de datos:
...
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);
}
}
Aunque los controladores y los servicios estén definidos, Nest sigue sin saber que existen y, por tanto, no creará una instancia de esas clases.
Para remediarlo, debemos añadir los controladores al archivo app.module.ts
, y añadir los servicios a la lista providers
: A continuación, exportaremos el esquema y los modelos en el AppModule
y registraremos el ServeStaticModule
. Esto nos permite renderizar los archivos al cliente.
....
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],
})
Crear el middleware
En este punto, Nest ya sabe que existen los controladores y servicios de la aplicación. El siguiente paso es crear un middleware para proteger las rutas de vídeo de los usuarios no autentificados.
Para empezar, vamos a crear un archivo app.middleware.ts
en la carpeta /src
, y a añadir el siguiente fragmento:
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)
}
}
}
En este código, creamos una clase isAuthenticated
, que implementa el NestMiddleware
. Obtenemos el token del cliente en las cabeceras de la solicitud y lo verificamos. Si el token es válido, el usuario tiene acceso a las rutas de vídeo. Si el token no es válido, lanzamos una HttpException
.
A continuación, abriremos el archivo app.module.ts
y configuraremos el middleware. Excluiremos la ruta de transmisión, ya que estamos transmitiendo directamente desde un elemento de vídeo en el 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);
}
}
Ahora, vamos a ejecutar el siguiente comando para iniciar el servidor NestJS:
npm run start:dev
Construir el front end de la aplicación React
Para agilizar esta parte del tutorial, he creado un archivo zip para que puedas descargar la interfaz de usuario del frontend de la aplicación. Para empezar, haz clic en este botón y centrémonos en consumir la API y la lógica de la aplicación.
Crear el inicio de sesión
Con la interfaz de usuario en marcha, vamos a manejar la lógica para registrar a los usuarios en la aplicación. Abre el archivo Component/Auth/Signin.js
, e importa axios
y useNavigation
:
...
import axios from 'axios';
import { useNavigate } from "react-router-dom"
...
En este código, utilizamos axios
para realizar las peticiones de la API al backend. useNavigation
se utiliza para redirigir a los usuarios después de un inicio de sesión exitoso.
Ahora, vamos a crear una función manejadora handleSubmit
con el siguiente fragmento:
...
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')
}
};
...
En este código, desestructuramos setIsLoggedIn
de nuestras props
, creamos un estado errorMessage
para mostrar mensajes de error a los usuarios durante el inicio de sesión. A continuación, utilizamos la API formData
para obtener los Formdata
del usuario de los campos de texto y utilizamos axios
para enviar una solicitud .post
al backend.
Comprobamos el estado de la respuesta para ver si el inicio de sesión fue exitoso. Si el inicio de sesión tiene éxito, guardamos el token que se envió al usuario en el localStorage
del navegador, restablecemos el estado setIsLoggedIn
a true y redirigimos al usuario a la página de vídeo. Un inicio de sesión fallido dará lugar a una respuesta 401(No autorizado). En este caso, mostraremos el mensaje de error al usuario.
A continuación, añadiremos un evento onSubmit
al componente del formulario y enlazaremos el manejador handleSubmit
.
...
<Box component="form" onSubmit={handleSubmit} noValidate sx={{ mt: 1 }}>
...
Si hay un errrorMessage
, lo mostraremos al usuario:
<Typography component="p" variant="p" color="red">
{errrorMessage}
</Typography>
Crear cuentas de usuario
Ahora, estamos preparados para registrar a los usuarios en la aplicación. Vamos a crear un componente de Registro que permita a los usuarios crear una cuenta. Abre el componente/Auth/Signup.js
, e importa axios
y useNavigate
:
...
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
...
A continuación, crearemos una función manejadora handleSubmit
con el siguiente fragmento:
...
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('/')
};
...
En este código, desestructuramos setIsLoggedIn
de las props
y creamos un estado errorMessage
para mostrar mensajes de error a los usuarios durante el inicio de sesión. A continuación, utilizamos la API formData
para obtener los datos de entrada del usuario de los campos de texto del formulario y enviamos una solicitud de publicación al backend utilizando axios
. Tras el inicio de sesión, redirigimos al usuario a la página de inicio de sesión.
A continuación, añadiremos un evento onSubmit
al componente for y enlazaremos el manejador handleSubmit
que acabamos de crear.
Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
Añadir vídeos a la biblioteca
Ahora que los componentes de autentificación del usuario están creados, vamos a dar a los usuarios la posibilidad de añadir vídeos a la biblioteca.
Empezaremos abriendo el Component/Navbar/Header.js
, e importando axios
:
...
import axios from 'axios';
...
A continuación, desestructuraremos el estado isLoggedIn
de las propiedades y crearemos tres variables React.useState
para el vídeo, la imagen de portada y el título.
...
const [videos, setVideos] = React.useState("");
const [cover, setCover] = React.useState("");
const [title, setTitle] = React.useState("")
...
Ahora crearemos una función manejadora de submitForm
. En nuestra función submitForm
, evitaremos la recarga por defecto del formulario, y obtendremos la información del envío del formulario utilizando la API formData
. Para autorizar al usuario a acceder a los puntos finales del vídeo, obtendremos el token del usuario del localStorage
del navegador, y enviaremos una petición HTTP .post con 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
})
})
}
...
A continuación, enlazaremos el controlador de submitForm
a un evento onSubmit
, y enlazaremos la variable de estado de entrada a un evento onChange
. El componente del formulario debería tener este aspecto:
<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>
Visualización de la lista de vídeos
Vamos a crear un componente VideoList
para mostrar los vídeos a los usuarios. Abre el archivo Component/Video/VideoList.js
, importa axios
, useParams
, useEffect
y useNavigate
.
//javascript
...
import { Link, useNavigate } from 'react-router-dom'
import axios from 'axios';
...
A continuación, crearemos un estado de videos
para almacenar los vídeos y un objeto navigate
para redirigir a los usuarios a la página de inicio de sesión cuando su token caduque:
...
const [videos, setVideos] = React.useState([])
const navigate = useNavigate();
...
Utilizaremos React.useState
para enviar una solicitud de obtención a la API cuando se monte el componente. Obtendremos el token del usuario de localStorage
y usaremos axios para enviarlo en las cabeceras de la petición a la 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]);
...
A continuación, recorreremos la lista de vídeos en el estado de videos
y mostraremos la lista a los usuarios. Utilizaremos el component
para crear un enlace a la página de flujo de vídeo, analizando el vídeo en la 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>
})}
...
Transmisión de los vídeos
Ahora, vamos a crear un componente para transmitir cualquier vídeo que el usuario seleccione. Abre el archivo Component/Video/Video.js
e importa useNavigation
y useParams
y axios
. Utilizaremos useNavigation
y useParams
para obtener el identificador del vídeo que el usuario quiere transmitir.
import { useParams, useNavigate } from 'react-router-dom';
import axios from 'axios';
Enviaremos una petición GET
con axios
con el videoId
en el parámetro URL y el token del usuario en las cabeceras de la petición para la autorización.
Si el token no es válido, restableceremos el estado isLoggedIn
y redirigiremos al usuario a la página de inicio de sesión.
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]);
Ahora, mostraremos los detalles del vídeo a los usuarios, y analizaremos la URL del vídeo en el elemento vídeo para transmitir el vídeo:
<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>
Desplegar la aplicación
Ahora, asegurándonos de que estamos en el directorio del frontend
, vamos a ejecutar el siguiente comando para desplegar la aplicación:
npm start
Conclusión
En este tutorial, presentamos NestJS como un framework para construir aplicaciones Node.js escalables. Demostramos este concepto construyendo una aplicación de streaming de vídeo full stack utilizando NestJS y React. El código compartido en este tutorial puede ampliarse añadiendo más estilos a la interfaz de usuario y también añadiendo más componentes.
Recuerda también, que cualquier duda que se te plantée puedes consultarla directamente con nostros mediante el formulario de aquí abajo de comentarios. Estaremos encantados de ayudarte en todo lo que necesites.
El código completo del proyecto utilizado en este artículo está disponible en este enlace. Siéntete libre descargarlo y hacer lo que quieras con él.
Deja una respuesta