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?

Why/Why not?

File Upload Approaches for GraphQL

$ 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

3. File Uploads through GraphQL

— GraphQL Server on NestJS

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)

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

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