Nowadays, video streaming is one of the main ways of consuming and sharing content. In this article, we explore the fundamental concepts of building a REST API for uploading videos to the server and streaming them using NestJS and Prisma. Check out this repository if you want to see the full code from this article. Uploading videos NestJS makes it very straightforward to store the files on the server with the FileInterceptor. videos.controller.ts import { Controller, Post, UseInterceptors, UploadedFile, } from '@nestjs/common'; import { Express } from 'express'; import { VideosService } from './videos.service'; import { FileInterceptor } from '@nestjs/platform-express'; import { diskStorage } from 'multer'; @Controller('videos') export default class VideosController { constructor(private readonly videosService: VideosService) {} @Post() @UseInterceptors( FileInterceptor('file', { storage: diskStorage({ destination: './uploadedFiles/videos', }), }), ) async addVideo(@UploadedFile() file: Express.Multer.File) { return this.videosService.create({ filename: file.originalname, path: file.path, mimetype: file.mimetype, }); } }Whenever we make a valid POST request to the API, NestJS stores the uploaded videos in the ./uploadedFiles/videos directory. In one of the previous parts of this series, we created a custom interceptor that allows us to avoid repeating some parts of our configuration whenever we need more than one endpoint that accepts files. It also allows us to use environment variables to determine where to store files on the server. videos.controller.ts import { FileInterceptor } from '@nestjs/platform-express'; import { Injectable, mixin, NestInterceptor, Type } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; import { diskStorage } from 'multer'; interface LocalFilesInterceptorOptions { fieldName: string; path?: string; fileFilter?: MulterOptions['fileFilter']; } function LocalFilesInterceptor( options: LocalFilesInterceptorOptions, ): Type { @Injectable() class Interceptor implements NestInterceptor { fileInterceptor: NestInterceptor; constructor(configService: ConfigService) { const filesDestination = configService.get('UPLOADED_FILES_DESTINATION'); const destination = `${filesDestination}${options.path}`; const multerOptions: MulterOptions = { storage: diskStorage({ destination, }), fileFilter: options.fileFilter, }; this.fileInterceptor = new (FileInterceptor( options.fieldName, multerOptions, ))(); } intercept(...args: Parameters<NestInterceptor['intercept']>) { return this.fileInterceptor.intercept(...args); } } return mixin(Interceptor); } export default LocalFilesInterceptor; Above, we are using the mixin pattern. If you want to know more, check out API with NestJS #57. Composing classes with the mixin pattern To use our custom interceptor, we need to add UPLOADED_FILES_DESTINATION to our environment variables. app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi'; import { VideosModule } from './videos/videos.module'; @Module({ imports: [ // ... ConfigModule.forRoot({ validationSchema: Joi.object({ // ... UPLOADED_FILES_DESTINATION: Joi.string().required(), }), }), VideosModule, ], controllers: [], providers: [], }) export class AppModule {} .env # ... UPLOADED_FILES_DESTINATION=./uploadedFilesThanks to all of the above, we can now take advantage of our custom interceptor in the videos controller. videos.controller.ts import { Controller, Post, UseInterceptors, UploadedFile, BadRequestException, } from '@nestjs/common'; import { Express } from 'express'; import LocalFilesInterceptor from '../utils/localFiles.interceptor'; import { VideosService } from './videos.service'; @Controller('videos') export default class VideosController { constructor(private readonly videosService: VideosService) {} @Post() @UseInterceptors( LocalFilesInterceptor({ fieldName: 'file', path: '/videos', fileFilter: (request, file, callback) => { if (!file.mimetype.includes('video')) { return callback( new BadRequestException('Provide a valid video'), false, ); } callback(null, true); }, }), ) addVideo(@UploadedFile() file: Express.Multer.File) { return this.videosService.create({ filename: file.originalname, path: file.path, mimetype: file.mimetype, }); } } Storing the information in the database Once we have the file saved on our server, we need to store the appropriate information in our database, such as the path to the file. To do that, let’s create a new table. videoSchema.prisma model Video { id Int @id @default(autoincrement()) filename String path String mimetype String }We also need to create the appropriate SQL migration.npx prisma migrate dev --name add-video-table If you want to know more about migrations with Prisma, go to API with NestJS #115. Database migrations with Prisma Thanks to defining the new table with Prisma, we can now store the information about a particular video in the database. videos.service.ts import { Injectable } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { VideoDto } from './dto/video.dto'; @Injectable() export class VideosService { constructor(private readonly prismaService: PrismaService) {} create({ path, mimetype, filename }: VideoDto) { return this.prismaService.video.create({ data: { path, filename, mimetype, }, }); } } Streaming videos The most straightforward way to stream files is to create a readable stream using the path to our file and the StreamableFile class. videos.service.ts import { Injectable, NotFoundException, StreamableFile, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service';a import { createReadStream } from 'fs'; import { join } from 'path'; @Injectable() export class VideosService { constructor(private readonly prismaService: PrismaService) {} async getVideoMetadata(id: number) { const videoMetadata = await this.prismaService.video.findUnique({ where: { id, }, }); if (!videoMetadata) { throw new NotFoundException(); } return videoMetadata; } async getVideoStreamById(id: number) { const videoMetadata = await this.getVideoMetadata(id); const stream = createReadStream(join(process.cwd(), videoMetadata.path)); return new StreamableFile(stream, { disposition: `inline; filename="${videoMetadata.filename}"`, type: videoMetadata.mimetype, }); } // ... } If you want to know more about the StreamableFile class, check the following articles: API with NestJS #54. Storing files inside a PostgreSQL database API with NestJS #55. Uploading files to the server videos.controller.ts import { Controller, Get, Param } from '@nestjs/common'; import { VideosService } from './videos.service'; import { FindOneParams } from '../utils/findOneParams'; @Controller('videos') export default class VideosController { constructor(private readonly videosService: VideosService) {} // ... @Get(':id') streamVideo(@Param() { id }: FindOneParams) { return this.videosService.getVideoStreamById(id); } }In our frontend application, we need to use the