File upload with urql and apollo-server
Graphql is getting really popular these days, which enables developers to build flexible data querying endpoints. I have followed a great tutorial from Ben Awad (https://www.youtube.com/watch?v=I6ypD7qv3Z8&t=47035s) and build the forum with the basic post and vote functions.
Some context: I am using Next.js and urql for the frontend. Urql is a light weighted framework GraphQL client. Express and apollo-server-express for the backend. AWS S3 for holding static files.
As the eager to pick up new technology, I decided to keep adding some common features for the forum to help me learn more about GraphQL. So the first feature coming into my mind is uploading images.
How to handle file uploading with GraphQL?
It is not well supported. Let’s see what we need to do with urql and our backend.
The upload flow for my case is sending the file from the React app to our GraphQL server and then upload to the S3 bucket, finally recording the location of the file to our database.
Frontend
I am using graphql-codegen
, so my mutation looks like this. If you are only doing file uploading, we can remove text and title.
mutation createPost($text: String!, $title:String!, $file: Upload!) { createPost(text: $text, title: $title, file: $file) { id }}
Upload
is a scalar provided by graphql-upload
which is needed on the backend, you don’t need to worry about it here. And we need to make an HTTP post request with multipart/form-data containing our meta-data and our source file. For urql case, we need to add @urql/exchange-multipart-fetch
to enable file uploading. Then you should be able to
import { multipartFetchExchange } from '@urql/exchange-multipart-fetch';
and replace the original fetchExchange
to the multipartFetchExchange
. (reference:
The final step on the frontend is adding file upload input into our form component.
<Formik ...some formik stuff>{({ values, setFieldValue, isSubmitting }) => (<Form>
...some other input fields<Inputtype='file'accept='image/*'onChange={({ target: { validity, files } }) => {if (validity.valid && files) {setFieldValue('file', files[0]);
// set 'file' of the form data as files[0]}}}/>...here should have a button
</Form>)}
</Formik>
Backend
Apollo-server(-express) has built-in graphql-upload
, but it is version 8 which is really outdated. Now it is version 11, so we need to disable the old built-in graphql-upload
, and use the nowadays implementation of the library
const apolloServer = new ApolloServer({schema: await buildSchema({resolvers: [],validate: false}),context: ({ req, res }) => ({req,res}),uploads: false // here});
After disabled the uploads from apollo-server, we can add the middleware
app.use(graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));
Then, we should be able to get proper file input from our client-side. Next step, we are gonna accept the file and upload it to AWS S3.
@Mutation(() => Post)@UseMiddleware(isAuth)async createPost(@Arg('text', () => String) text: string,@Arg('title', () => String) title: string,@Arg('file', () => GraphQLUpload, { nullable: true }) file: FileUpload, // take care of here@Ctx() { req }: MyContext // a self defined context type): Promise<(Post & UploadedFileResponse) | Post> {let fileRet;const post = await Post.create({...}).save();// save post to database via typeORMif (file) {const s3Uploader = new AWSS3Uploader({accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,destinationBucketName: process.env.AWS_BUCKET_NAME as string,region: 'us-east-2'});fileRet = await s3Uploader.singleFileUpload.bind(s3Uploader)(file);const fileObj = await File.create({postId: post.id,...fileRet} as Object).save() // save file url to database via typeORM}const retObject = { ...post, ...fileRet };return retObject;}
Two parts needed to take care of are GraphQLUpload
and FileUpload
, we should import them from graphql-upload
not apollo-server
or apollo-server-express
. AWSS3Uploader
is a class taking AWS config as input and create an S3 instance and having a file upload function.
Finally, we should be able to see our file on the S3 console and also some information in our database.