Step by Step Guide to Uploading Multiple Files with Graphql in Nestjs
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
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
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 Profile
module’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.
Don’t forget to set the preflight
headers to true if you are using apollo > 4
If you send the request, you should see the result as shown below.
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]})
...
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
[4] https://graphql-compose.github.io/docs/guide/file-uploads.html