Step by Step Guide to Uploading Multiple Files with Graphql in Nestjs

Shovan Raj Shrestha
8 min readAug 27, 2023

--

Today I will demonstrate how we can upload multiple files with graphql in nestjs server. You can find the complete sample project in the following github link.

Github Link: https://github.com/shovan777/gql-uploader

Prerequisites:

Basic understanding of typescript, nestjs, graphql and typeorm is sufficient for the project. Also, graphql-upload is the package required for the upload.

Let’s create a new nest application

nest n gql-uploader

Now, let’s generate a new graphql resource

nest g res
Generating resource with nestjs-cli

Please don’t forget to select graphql code first approach as shown in the above image.

Install all relevant packages for graphql and file uploads. I installed v13.0.0 of graphql-upload as it doesn’t give any import error.

npm i @nestjs/graphql graphql-tools graphql graphql-upload@13 @types/graphql-upload@8 @nestjs/apollo @apollo/server

Let’s start the application to verify our steps till now

npm run start:dev

You should see an output like this

Start nestjs application

Connect to a database. I am using sqlite for demonstration but we should use postgres, mysql, etc RDMS in production environments.

npm install typeorm @nestjs/typeorm sqlite3

Configure typeorm to connect to sqlite. Also, configure graphql module with apollo. Add the code in src/app.module.ts


...
@Module({
imports: [
ProfilesModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: 'profileDB',
entities: [__dirname + "/**/*.entity{.ts,.js}"],
synchronize: true
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
path: '/graphql',
autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
})
],
controllers: [AppController],
providers: [AppService],
})
...

Configure middleware for graphql-upload. The maxFileSize is the max allowed size of uploaded files in bytes. Here I have set the limits to only 1mb which you can increase to your requirement. Add the code in src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { graphqlUploadExpress } from 'graphql-upload';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(graphqlUploadExpress({maxFileSize:1000000, maxFiles: 5}))
await app.listen(3001);
}
bootstrap();

Create profile and profile image entity to store the image paths. Single profile can have many images as such ProfileImage has manyToOne relation to Profile entity. I will be using code-first approach for the demonstration but schema based approach is also a viable option. Its a personal preference since I find it easier to code in code-first approach. Add the code in src/profiles/entities/profile.entity.ts

import { ObjectType, Field, Int } from '@nestjs/graphql';
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';

@ObjectType()
@Entity()
export class Profile {
@Field(() => Int, { description: 'Unique id of the profile' })
@PrimaryGeneratedColumn()
id: number;

@Field(() => [ProfileImage], {description: 'Profile Image'})
@OneToMany(() => ProfileImage, (profileImg) => profileImg.profile)
images: ProfileImage[];
}

@ObjectType()
@Entity()
export class ProfileImage {
@Field(() => Int, {description: 'Unique id of profile image'})
@PrimaryGeneratedColumn()
id: number;

@Field({description: 'Profile Image'})
@Column()
imageURL: string;

@Field(() => Profile)
@ManyToOne(() => Profile, (profile) => profile.images)
profile: Profile;
}

Also, add the entities to the Profilemodule’s imports. Add the code in src/profiles/profile.module.ts

import { Module } from '@nestjs/common';
import { ProfilesService } from './profiles.service';
import { ProfilesResolver } from './profiles.resolver';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Profile, ProfileImage } from './entities/profile.entity';

@Module({
providers: [ProfilesResolver, ProfilesService],
imports: [TypeOrmModule.forFeature([Profile, ProfileImage])],
})
export class ProfilesModule {}

Now let’s create DTO to accept the profile and its image data. Although graphql-upload provides GraphQLUpload scalar type typeorm treats it as value and we need to define our own scalar type as such. There are other approaches to achieve the same but I found this way the easiest and most manageable. Add the code in src/scalars/upload.scalar.ts

import { Scalar } from '@nestjs/graphql';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';

@Scalar('Upload')
export class Upload {
description = 'Upload files';

parseValue(value) {
return GraphQLUpload.parseValue(value);
}

serialize(value) {
return GraphQLUpload.serialize(value);
}

parseLiteral(ast) {
return GraphQLUpload.parseLiteral(ast, ast.value);
}
}

The Upload scalar can be used whenever we require a file upload field. We use it in our profile image field. Add the code in src/profiles/dto/create-profile.input.ts

import { InputType, Int, Field,  } from '@nestjs/graphql';
import { Upload } from 'src/scalars/upload.scalar';

@InputType()
export class CreateProfileInput {
@Field(() => [Upload], { description: 'Input for the profile image files.' })
images: Upload[];
}

Now, lets add create resolver and services to handle the file data. Let’s console.log file data for now. Add the code in src/profiles/profiles.resolver.ts and then, src/profiles/profiles.service.ts .

...
@Mutation(() => Profile)
createProfile(
@Args('createProfileInput') createProfileInput: CreateProfileInput,
) {
return this.profilesService.create(createProfileInput);
}
...
...
create(createProfileInput: CreateProfileInput) {
console.log(createProfileInput);
return 'This action adds a new profile';
}
...

I couldn’t make file upload request in gqlid so, let’s use postman to make the request. Create a post request in postman and select formdata in the Body . I had great difficulty in making the file upload request for graphql and finally, I could do it postman.

You can upload files as multipart/formdata. In body you need to make the graphql upload request with key and values of form-data. You need three keys: operation, map, <int> and values like so:

operation

{"query": "mutation ($file: Upload!){createProfile(createProfileInput: {images: [$file,$file]}) {id images{id imageURL}}}", "variables":{"file":null}}

map

{ "0": ["variables.file"] }

<int>

Choose file field in the key dropdown and place your files here. If you have other files you can increment the int and add the files and so on.

Graphql upload request in postman

Don’t forget to set the preflight headers to true if you are using apollo > 4

Headers in postman

If you send the request, you should see the result as shown below.

File data promise printed in teminal

Hurray!!! 🎉 🎉 We have successfully made the request with file upload data. All that remains is to store the file and store the filepath in the db. I will demonstrate storing files in local storage but in production system you often store files in storage buckets like s3, volumes , etc. by cloud providers like aws, digital ocean, etc.

graphql-upload provides the file as a stream which you can process to save as a file. It also gives you the filename which you can use to store the file as shown below. Add the code in src/utils/upload.ts.

import { NotFoundException } from "@nestjs/common";
import { createWriteStream, mkdirSync } from "fs";
import { join } from "path";
import { finished } from "stream/promises";

export const uploadFileStream = async (readStream, uploadDir, filename) => {
const fileName = filename;
const filePath = join(uploadDir, fileName);
console.log(`file path: ${filePath}`);
mkdirSync(uploadDir, { recursive: true });
const inStream = readStream();
const outStream = createWriteStream(filePath);
inStream.pipe(outStream);
await finished(outStream)
.then(() => {
console.log('file uploaded');
})
.catch((err) => {
console.log(err.message);
throw new NotFoundException(err.message);
});
};

In the profile create service you first save the profile entity and then, save the files using the above uploadFileStream function and finally, save the ProfileImageEntity with filepaths and profile entity. Since, the files are saved asynchronously we need Promise.all() to handle all the file save promises. This ensures that the files are saved properly without any error. We can also skip this step and not return the saved images in the response of our create mutation, in cases where we have too many files of large size and we don’t require response or if we don’t need to ensure that the files have been saved. Add the code in src/profiles/profiles.service.ts. In the filename below I have also attached the date.now() i.e current date to make the filename unique to handle cases user may upload files with same name.

...
// you should probably read this from env in production
uploadDir = 'uploads';
async create(createProfileInput: CreateProfileInput): Promise<Profile> {
let profileInputData = {
...createProfileInput,
images: null,
};
const profileData: Profile = await this.profileRepo.save({
...profileInputData,
});
const imagePaths = createProfileInput.images.map(async (image, index) => {
const imageFile: any = await image;
const fileName = `${Date.now()}_${index}_${imageFile.filename}`;
const uploadDir = join(
this.uploadDir,
`profiles_${profileData.id}`,
'images',
);
const filePath = await uploadFileStream(
imageFile.createReadStream,
uploadDir,
fileName,
);
return filePath;
});
const profileImages: Promise<ProfileImage>[] = imagePaths.map(
async (imagePath) => {
return await this.profileImageRepo.save({
imageURL: await imagePath,
profile: profileData,
});
},
);
profileInputData = {
...profileInputData,
images: await Promise.all(profileImages),
};
profileData.images = profileInputData.images;
return profileData;
}
...

Now, let’s test our resolver in postman and see the result.

Voila`!!!!!!!! 🎉 🎉, we have successfully uploaded multiple files with graphql. However, there is still a small step required to make the output complete image url instead of just the file path. Since, the host can change based on the server ip address we need to dynamically attach the hostname and also, our nest server should be able to serve static files. This sure sounds like a handful. Fortunately, nestjs has fieldmiddleware which makes this a breeze. Here, I have statically returned the path but in production we must assign dynamic path like domain name from env variables. Add the code in src/middleware/pathFinderMiddleware.ts.

import { FieldMiddleware, MiddlewareContext, NextFn } from '@nestjs/graphql';

export const pathFinderMiddleware: FieldMiddleware = async (
ctx: MiddlewareContext,
next: NextFn,
) => {
let filePath: string = await next();
return `http://localhost:3001/${filePath}`;
};

Use the pathFinderMiddleware in the middleware option of field in entity like so in src/profiles/entities/profile.entity.ts.

...

@Field({description: 'Profile Image', middleware: [pathFinderMiddleware]})

...
Image url with full path

Now, you need to serve static files with nestjs for the above link to work. In production environment you shouldn’t serve static files instead using other proxy servers like nginx or cloud server like cloudflare is much better for file hosting.

Install nestjs static package

npm install --save @nestjs/serve-static

Then, import ServeStaticModule in the root module of the project. Provide the paths to the dir where your files are stored in rootPath and also provide the path where you are serving the files in serveRoot . Again, note that the paths must be read from env in production. Add the code in src/app.module.ts.

...
ServeStaticModule.forRoot({
rootPath: join(process.cwd(), 'uploads'),
serveRoot: '/uploads',
serveStaticOptions: {
extensions: ['jpg', 'jpeg', 'png', 'gif'],
index: false,
},
...

Congratulations!!!!!!!! 🥳🥳🌟🌟 now you can serve the files to any frontend as per your requirement. You can find the complete code here.

References:

[1] https://vortac.io/2020/05/09/uploading-files-with-nestjs-and-graphql/

[2] https://dilushadasanayaka.medium.com/nestjs-file-upload-with-grapql-18289b9e32a2

[3] https://www.apollographql.com/blog/graphql/file-uploads/with-react-hooks-typescript-amazon-s3-tutorial/

[4] https://graphql-compose.github.io/docs/guide/file-uploads.html

--

--