Upload files in NestJS & GraphQL

GraphQL Nasty part

Upload files in NestJS & GraphQL

The challenge with uploading files with GraphQL is that GraphQL isn't friendly for handling multipart requests and a bit of configuration is needed. There might be a better approach out there, nevertheless, I'll show you my way that gets things done.

Note

You can decide to use the Apollo version to navigate around the security restrictions introduced on version 4 e.g. Cors, CSRF, etc

You should have a basic NestJS project setup, you can choose to follow along on NestJS 101.

Follow along on the GitHub repo, note that this is a focus on NestJS GraphQL only. This repo contains other parts as well.

Getting Started

Set up a basic NestJS project. You can skip this part if you already set up the project.

On your terminal:

npm install -g @nestjs/cli
nest new file-uploads

You should have a folder structure like this:

project-name/
├── src/
│   ├── app.controller.ts
│   ├── app.controller.spec.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   ├── main.ts
├── test/
├── node_modules/
├── package.json
├── tsconfig.json
└── nest-cli.json

Now let's configure GraphQL and Apollo server(Apollo helps in data fetching & management). Also, let's do a quick setup for file upload. We need graphql-upload package to manage file upload into our app. Also, you've to configure this as a middleware to enable files to be integrated into the GraphQL URL/environment.

Install the following dependencies:

yarn add @apollo/server graphql-tools graphql-upload@13.0.0 @types/graphql-upload@8.0.12

In the app.module.ts file, configure graphql-upload to be used by the app

import { Logger } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'

import { graphqlUploadExpress } from 'graphql-upload'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)

  app.enableCors()

  app.use(graphqlUploadExpress())

  app.useGlobalPipes()

  const globalPrefix = 'api'
  app.setGlobalPrefix(globalPrefix)

  const port = process.env.PORT || 4000
  await app.listen(port)

  Logger.log(`🚀 Application is running on: ${await app.getUrl()}/graphql)`)
}

bootstrap()

In the app.module.ts , make sure to put the default uploads: false and disable default playground(playground will depend on the version of apollo-server you've installed, for me am using version 4). Also at the export of app, configure the Middleware to manage file uploads

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { GraphQLModule } from '@nestjs/graphql'
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'
import { graphqlUploadExpress } from 'graphql-upload'
import {
  ApolloServerPluginLandingPageLocalDefault,
} from '@apollo/server/plugin/landingPage/default'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { FilesModule } from './files/files.module'

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    GraphQLModule.forRootAsync<ApolloDriverConfig>({
      imports: [ConfigModule],
      driver: ApolloDriver,
      useFactory: async () => ({
        playground: false,
        uploads: false,
        plugins: [ApolloServerPluginLandingPageLocalDefault({ footer: false })],
        autoSchemaFile: true,
      }),
    }),
    FilesModule
  ],
  controllers: [AppController],
  providers: [AppService],
  exports: [],
})

export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }))
      .forRoutes('graphql')
  }
}

Now, that we are done with the configurations, the complex part is over, let's go to the sweet stuff, file uploads.

Create new filesUpload module using nestjs-cli

nest g module filesUpload
nest g module resolver
nest g module service

Once, the above is done, then filesUploadModule should be in the app.module.ts.Now, let's create the necessary mutations. We need to upload files to AWS S3, and Cloudinary and save files from the frontend to the backend folder. Make sure to rely on graphql-upload to manage the file types.

Am going to use streams to manage file uploads, especially large file. Files have CreateReadStream from the file system that helps to read files in chunks rather than the entire file, check out this article on readable streams.

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { FileUpload, GraphQLUpload } from 'graphql-upload'
import { FilesUploadService } from './filesUpload.service';

@Resolver()
export class FilesUploadResolver {

    constructor(private readonly filesUploadService: FilesUploadService) {}

    @Query(() => String) 
    async Test() {
        return 'here we go'
    }

    @Mutation(() => Boolean)
    async AWSfileUpload(@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload) {
        return this.filesUploadService.AWSfileUpload(file)
    }

    @Mutation(() => Boolean)
    async CloudinaryfileUpload(@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload) {
        return this.filesUploadService.CloudinaryfileUpload(file)
    }

    @Mutation(() => Boolean)
    async LocalfileUpload(@Args({ name: 'file', type: () => GraphQLUpload }) file: FileUpload) {
        return this.filesUploadService.LocalfileUpload(file)
    }
}

Then finally, we can create the service to now manage the file uploads to the residential location. Make sure to configure the AWS Key and secret key using AWS SDK, and Cloudinary credentials. Instead of 3 functions/mutations, you can have one mutation then have a variable identifier to upload to specific platform using conditional statements

import { Injectable } from '@nestjs/common';
import { FileUpload } from 'graphql-upload'
import { S3Client } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import { v2 as cloudinary } from 'cloudinary';
import { join } from 'path';
import { createWriteStream } from 'fs';

@Injectable()
export class FilesService {
    private readonly s3Client: S3Client
    private AWSRegion: string

    constructor() {
        this.AWSRegion = `eu-west-1`
        this.s3Client = new S3Client({ region: this.AWSRegion })

        cloudinary.config({ 
            cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 
            api_key: process.env.CLOUDINARY_API_KEY, 
            api_secret: process.env.CLOUDINARY_API_SECRET
        });
    }


    async AWSfileUpload(file: FileUpload) {
        try {
            // s3 functionality
            const key = `fileLocationFolder/${Date.now()}-${file?.filename}`
            const { createReadStream, mimetype} = file;

            const fileStream = createReadStream()

            const upload = new Upload({
                client: this.s3Client,
                params: {
                  Bucket: `S3BucketName`,
                  Key: key,
                  Body: fileStream,
                  ContentType: mimetype,
                },
              })

            await upload.done()
            return true
        } catch (error) {
            console.log('Error occurred on AWSfileUpload-files.service: ', error.message)
        }
    }

    async CloudinaryfileUpload(file) {
        try {
            // cloudinary functionality
            // some other
            // upload_large
            // upload 
            var upload_stream= cloudinary.uploader.upload_stream({folder: 'file-series', tags: 'files-series'},function(err,image) {
                if (err){ 
                        console.warn(err);
                        return false;
                    }
                console.log("* ", image.public_id);
                console.log("* ", image.url);
              });

            const { createReadStream } = file;

            const fileStream = createReadStream()

            await fileStream.pipe(upload_stream);
            return true;
        } catch (error) {
            console.log('Error occurred on CloudinaryfileUpload-files.service: ', error.message)
            return false
        }
    }

    async LocalfileUpload(file: FileUpload) {
        try {
            // Save to local root folder functionality
            const localFilePath = join(process.cwd(), 'uploads', `${Date.now()}-${file?.filename}`)

            return new Promise<void>((resolve, reject) => {
                const writeStream = createWriteStream(localFilePath)

                file.createReadStream().pipe(writeStream).on('finish', () => resolve).on('error', (error) => {
                    console.error('Error occurred - ', error.message)
                    reject(error)
                })
            })
        } catch (error) {
            console.log('Error occurred on LocalfileUpload-files.service: ', error.message)
        }
    }
}

The backend part is now complete and we can upload files. Start the backend:

npm run start

Your graphql playground should look something like this, once you select the mutation, then there's files to select on the variables input

We shall connect to the frontend on our next series, stay tuned

Did you find this article valuable?

Support Nicanor Talks Web by becoming a sponsor. Any amount is appreciated!