File Uploads on GraphQL: Why or Why not

Moon (Made with Blender)
The Moon (Moon — Apollo — GraphQL, “get it? 😂”), made with Blender [@photosbysaurav on Instagram]

What are we trying to do?

GraphQL request and responses are typically in JSON format even though the GraphQL Spec doesn’t mandate any format.

Why/Why not?

There’s no right or wrong here, but here are a few things to consider when you want to have file uploads and you also have a GraphQL API.

File Upload Approaches for GraphQL

Few ways to go about it —

$ cat upload.txt
hello this is a simple file to be uploaded
window.btoa('hello this is a simple file to be uploaded')
> 'aGVsbG8gdGhpcyBpcyBhIHNpbXBsZSBmaWxlIHRvIGJlIHVwbG9hZGVk'

2. All File Handling happens on a separate endpoint

This would mean that your files can be uploaded to a separate REST endpoint, either hand-written or something like a pre-signed URL upload to a Storage Account on Microsoft Azure/S3 on Amazon Web Services.

3. File Uploads through GraphQL

Finally! As mentioned earlier, some GraphQL implementations do allow uploading files with a multipart/form-data request format.

— GraphQL Server on NestJS

For NestJS, the GraphQL setup is fairly simple, read more about it here — docs.nestjs.com/graphql/quick-start

npm i graphql-upload && npm i -D @types/graphql-upload
import { Field, Int, ObjectType } from '@nestjs/graphql';@ObjectType()
export class Person {
@Field(() => Int)
id: number;
@Field()
firstName?: string;
@Field()
lastName?: string;
@Field(() => Int, { nullable: true })
coverPhotoLength?: number = null;
@Field(() => String, { nullable: true })
coverPhoto?: string;
private _coverPhoto?: Buffer;
}
import { NestFactory } from ‘@nestjs/core’;
import { AppModule } from ‘./app.module’;
import { graphqlUploadExpress } from ‘graphql-upload’;
async function bootstrap() {
const port = process.env.PORT || 8080;
const app = await NestFactory.create(AppModule);
// Allow maximum file size of 2 Megabytes —
// change based on your needs and
// what your server can handle
app.use(graphqlUploadExpress({ maxFileSize: 2 * 1000 * 1000 }));
await app.listen(port);
console.log(`App running at ${await app.getUrl()}`);
}
bootstrap();
import { Args, Int, Mutation, Query, Resolver } from '@nestjs/graphql';
import { Person } from './person.model';
import { GraphQLUpload, FileUpload } from 'graphql-upload';
import * as fs from 'fs/promises';
@Resolver(() => Person)
export class PersonResolver {
person: Person;
public constructor() {
this.person = {
id: 1,
firstName: ‘Saurav’,
lastName: ‘Sahu’,
};
}
@Mutation(() => Int, { name: ‘coverPhoto’ })
async uploadCoverPhoto(
@Args(‘file’, { type: () => GraphQLUpload }) file: FileUpload,
): Promise<number> {
try {
const { createReadStream } = file;
const stream = createReadStream();
const chunks = [];
const buffer = await new Promise<Buffer>((resolve, reject) => {
let buffer: Buffer;
stream.on(‘data’, function (chunk) {
chunks.push(chunk);
});
stream.on(‘end’, function () {
buffer = Buffer.concat(chunks);
resolve(buffer);
});
stream.on(‘error’, reject);
});
const buffer = Buffer.concat(chunks); const base64 = buffer.toString(‘base64’);
// If you want to store the file, this is one way of doing
// it, as you have the file in-memory as Buffer
await fs.writeFile(‘upload.jpg’, buffer);
this.person.coverPhotoLength = base64.length;
this.person.coverPhoto = base64;
return base64.length;
} catch (err) {
return 0;
}
}
}

const stream = createReadStream();
const chunks = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
const buffer = Buffer.concat(chunks);

— GraphQL Server on .NET (HotChocolate)

HotChocolate, one of the most popular GraphQL libraries for .NET also has an implementation for File Uploads.

using HotChocolate.Types;
using BlogGraphQLFileUpload.GraphQL;
var builder = WebApplication.CreateBuilder(args);// Add services to the container.
builder.Services.AddControllers();
builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddType<UploadType>();
var app = builder.Build();// Configure the HTTP request pipeline.
app.UseAuthorization();
app.MapControllers();
app
.UseRouting()
.UseEndpoints(endpoints => { endpoints.MapGraphQL(); });
app.Run();
using BlogGraphQLFileUpload.Data;
using HotChocolate.Types;
namespace BlogGraphQLFileUpload.GraphQL;public class Mutation
{
public async Task<long?> coverPhoto(IFile file)
{
await using var stream = file.OpenReadStream();
var streamWriter = new FileStream(
"./output.jpg",
FileMode.OpenOrCreate
);
await stream.CopyToAsync(streamWriter); GlobalData.me.CoverPhotoLength = stream.Length; return GlobalData.me.CoverPhotoLength;
}
}

Testing your File Uploads

At the time of writing, Apollo Playground doesn’t support File Uploads through its UI. So you’re going to have to use Postman to test out your File upload

curl — location — request POST 'http://localhost:8080/graphql' \
— form 'operations="{\"query\": \"mutation updateProfilePhoto($file: Upload!) { coverPhoto(file: $file)} \", \"variables\": {\"file\": null}}"' \
— form 'map="{\"0\": [\"variables.file\"]}"' \
— form '0=@"./assets/grand-palais-mrsauravsahu.jpg"'

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Saurav Sahu

Saurav Sahu

Opinions are my own. Full Stack Engineer. CEO of “it was just working 🤷‍♂️”