File Uploads on GraphQL: Why or Why not

Note: The code mentioned in this post can be found in these repositories.

  • NestJS Reference – mrsauravsahu/blog-graphql-nestjs-fileupload
  • .NET Reference – mrsauravsahu/blog-graphql-dotnet-fileupload

If you just want to see how to do file uploads with GraphQL, just click here

GraphQL has become quite popular due to its various features fixing under/over fetching issues. It also allows for easy caching, federation, non-versioning APIs, subscriptions etc,.

For the modern Internet where data needs to be accessible on various types of applications running on various types of devices, GraphQL seems like a great way forward and also a good concept to put into your knowledge bag.

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.

All data fetching and uploading can be done easily with GraphQL and responses can also use GZIP for compression.

One thing GraphQL lacks (or rather doesn’t have a standard implementation for) is File Uploads.

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.

  • Standardization: Because typically GraphQL APIs use JSON format, they don’t require Content Negotiation. This means that File Uploads, which use a multipart format, can be tricky to standardize. Most GraphQL implementations do provide provisions to implement File Uploads through your GraphQL API however.

  • Fully-Featured: All GraphQL APIs will use a text-based response format, so file downloads will still require a separate endpoint. This means your file upload and download will become separated. Decide based on whether you’re fine with this or not.

  • All ingress at one-point: A good reason why you might want to use File Uploads through GraphQL is because you can still make sure all incoming data into your system is through the same endpoint. Not a strong argument, but traffic management does become easier.

File Upload Approaches for GraphQL

Few ways to go about it –

1. Files as strings

If your APIs deal with very small files, you can get away with a simple conversion from the binary representation of your file to a base64 string.

Let’s see a simple example. If your file has the following content.

$

cat

upload.txthello this is a simple file to be uploaded

Enter fullscreen mode

Exit fullscreen mode

You can use an input field to get the file in the Frontend and read its contents (with a FileReader perhaps) and then create a base64 string with the window.btoa WebAPI.

window

.

btoa

(

'

hello this is a simple file to be uploaded

'

)

>

'

aGVsbG8gdGhpcyBpcyBhIHNpbXBsZSBmaWxlIHRvIGJlIHVwbG9hZGVk

'

Enter fullscreen mode

Exit fullscreen mode

From now, your file can be treated as a base64 string so processing it is fairly similar to how you process regular strings in your application.

Note: As file sizes grow, your application needs to be able to handle larger strings as payload and response sizes will drastically increase.

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.

Let’s now see how this can be done with a NestJS GraphQL Server and a .NET GraphQL Server (with HotChocolate)

– GraphQL Server on NestJS

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

This uses the Apollo GraphQL Server, which does have support for File Uploads albeit through a different package.

So let’s install this package. This is the graphql-upload package, and because we’re using TypeScript, it’s good to also install the typings for it.

npm i graphql-upload 

&&

npm i

-D

@types/graphql-upload

Enter fullscreen mode

Exit fullscreen mode

NestJS uses TypeGraphQL behind the scenes, which means our GraphQL Schema can be generated from TypeScript classes. I have a basic Model here.

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

;

}

Enter fullscreen mode

Exit fullscreen mode

This is a basic model to store details about a user, or a person rather.

For our File Upload to work, we need to initialize the graphql-upload package in our main.ts

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

();

Enter fullscreen mode

Exit fullscreen mode

Let’s add a mutation which allows the consumer of our GraphQL API to upload a file and we’ll return the length of the file in bytes.

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

;

}

}

}

Enter fullscreen mode

Exit fullscreen mode

Here, the GraphQLUpload type creates a scalar in our GraphQL Schema which allows for uploading. As you can see, we get the stream in our handler and we can do any type of processing on it.

We’re putting the chunks of the multipart upload together and then writing to a file, but you can also pipe the readStream to a file directly. This is just to show that you can handle the raw bytes in your file.

However, this code looks a bit unwieldy due to the stream events, so thanks to a newer node feature, we can use a for await loop instead.

We can replace the stream.on calls with this –

...

const

stream

=

createReadStream

();

const

chunks

=

[];

for

await

(

const

chunk

of

stream

)

{

chunks

.

push

(

chunk

);

}

const

buffer

=

Buffer

.

concat

(

chunks

);

...

Enter fullscreen mode

Exit fullscreen mode

This is pretty neat, isn’t it.
So, that’s how you can implement File Uploads on your GraphQL API with NestJS.

GraphQL Server on .NET (HotChocolate)

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

At the time of writing, I was on an RC version of .NET 6. But this works for .NET 6.0.100 as well. Yay! this means there’s really less code.

This is my Program.cs

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

();

Enter fullscreen mode

Exit fullscreen mode

As you can see I’m setting up GraphQL with Services. To allow file uploads, I have to add the Upload Scalar to my GraphQL Schema. This is done with the builder.Services.AddType<UploadType>() call.

Now we can write a similar mutation to handle our File Upload, which is the Mutation class I have registered in this case.

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

;

}

}

Enter fullscreen mode

Exit fullscreen mode

HotChocolate gives you an IFile interface to work with, and you can get the stream from there. Now you have the power to process it however it makes sense for your application.

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

Shoutout to this answer on Stack Overflow – helped a lot – https://stackoverflow.com/a/61892790/5640343

You can also use the same thing with a curl command

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"'

Enter fullscreen mode

Exit fullscreen mode

Here, the map property maps our file and passes it on to our handler. You should also be able to get the mime-type and add more logic for those.

So, this was how you can do File Uploads with GraphQL, and also a few thoughts on if you really should? You can find the full code base in the links at the top.

Have a great one!

– Saurav, @mrsauravsahu everywhere.

Xổ số miền Bắc