Prisma Examples Series: File API

Handling files with Amazon S3

Hi everyone, this is the first post in my GraphQL Server examples series. (Actually, to be honest, this is my first post ever.) All of the examples will rely on Prisma and feature schema stitching. (I highly recommend reading this article first, if you have never heard of GraphQL schema stitching before.)

So, for the first tutorial I though about implementing a very simple File management API, which connects with Amazon S3 and synchronises database with our uploaded files.

I recommend you clone the example repository (repository) and follow along with the snippets, so that you can understand how all the parts fit the puzzle.

1.0 Setting up the S3 Bucket

First things first, the purpose of our little project is to store files. In order to store files we will use Amazon S3 file management service, which is super easy to use and gives a whopping 5GB of storage on the free tier.

In order to set up S3, we will first have to create Amazon AWS account, if you don’t have it yet, head over to the console and create one right away.

Amazon S3 stores files in separate units called Buckets . Buckets are like super folders where you can upload and store your files. To create a bucket navigate to the S3 section of your AWS Dashboard and create a new bucket by clicking that shiny blue button saying + Create Bucket. Pick a catchy name for you bucket and do not grant any public permissions under permissions tab to keep your things private.

1.1 Authorising S3

Each bucket is tied in with your account. In order to upload and access files within a bucket, we must first obtain permissions to do so. Amazon manages permissions with Identity and Access Management (IAM) service. Best way to manage permissions is by creating new user for your service.

Go to IAM and select Users tab on the left hand side. Click Add User and give it a name. Beneath check Programmatic access and press next. Select Attach existing policies directly and search for AmazonS3FullAccess. Check it and press next to review everything you have selected. Finish creating User and copy Access Key ID and Secret Access Key somewhere you can access them later.

1.2 Connecting S3 with Server

Enough of setting up, it’s time to deep dive into the code.

First, let’s connect S3 with our server. Amazon has kindly provided a whole SDK of tools which can be used to access all the services they offer. For the purpose of this example we will only use S3 part of it.

To upload files to S3 we use s3.upload() function, which returns untracable file URL. We save this information for later use with database synchronisation.

1.3 Uploading files

We are using multiparty to handle uploaded files. Files are received as multipart/form-data , each file being a separate part.

Each file is first examined for name, mime-type and size. We generate a unique secret for each file in order to make sure files in the Bucket don’t repeat their names. Each file is then uploaded to S3, returning the untracable access url, which is later used to sync database. In the end each file is added to the list of all files, which will be returned as JSON response, once all files are uploaded.

2.0 Prisma datamodel and config

The crucial part of our app is our database and the crucial part of our database is it’s data model. We define our data model using GraphQL SDL. Since our datamodel consists only of File type, our datamodel is rather short.

In order to use our datamodel, we also have to deploy it. We deploy it using prisma deploy command, which relies on prisma.yml config file.

Once deployed, Prisma will generate a schema in src/generated/database.graphql for us. Generated schema contains all the mutations and queries we need in order to queryFiles, createFiles, updateFiles and deleteFiles, we will use this later on with Prisma-binding. Alongside generated schema the endpoint of our database is provided. Remember the endpoint for later use.

2.1 GraphQL-Yoga and Prisma-binding

GraphQL Yoga is the melting pot of our application. To truly understand the logic of our application, we need to understand the capabilities of it and how it works.

Yoga consists of two parts — schema and resolvers. The schema part tells our app how the api should look like — it is like a link between our server and a client. The primary role of our app is to be able to search for files and get information about each one of them. We also want to be able to rename files and delete them if need be. Hence, the schema would look something like this.

Query and Mutation types are the two entry points of our API and are a must in every GraphQL Type System . Query properties tell the app what can be read and Mutation properties tell what can be written.

The second part of Yoga, resolvers, tell the app how each of the defined Queries and Mutations should be executed. In order to understand how resolvers in our app work, we must first take a look at Prisma binding and examine how Files are synchronised with our database.

Prisma-binding, simply put, abstracts away the GraphQL mutation and query part, by allowing us to call simple functions like binding.query.files() and binding.mutate.createFile() , instead of gloomy multi-lined queries. The following functions are generated from our database schema (src/generated/database.graphql) we have obtained before. To use Prisma-binding we must first tell it our database endpoint and copy our secret from config.

We use Prisma-binding to sync our Files between S3 and Prisma. Each time a File is uploaded to S3 and url is received a new instance of File is created.

In addition to file.ts file Prisma-binding is also used as a context of each call. Defined context is then passed down to resolvers as one of the arguments of the function, where we use it as connection between our resolvers/server and our database. (Yoga handles this for us.)

2.2 Resolvers and Mutations

Following the shape of our schema, resolvers implement each branch and leaf our schema “tree” the same way schema is defined. For each type and for each property we define in our schema.graphql we must define a resolver.

Since our File type already has a resolver from database schema, we only have to define resolvers for our Mutations and Queries. Both, Mutations and Queries, follow a similar structure of calling our database to get the infomation we want.

Since all the functions, provided by Prisma-binding, use the same formula, I will only explain one the example of context.db.mutation.updateFile() how it works. Take a look at the database/schema.graphql — the auto-generated schema of our database.

Under Mutations we can find updateFile field, which takes data and where as its arguments. ctx.db.mutation.updateFile({ data: { name }, where: { id } }, info) follows the same structure, taking the arguments of a query as the first function argument and a GraphQL Query (info) — which is basically what we want to get back — as the second. In our case info just copies everything user has requested when calling updateFile, but we could also query only specific fields, just like when creating a File, when we only queried { id } .

3.0 Putting it all together

The majority of complexity is abstracted away by GraphQL-Yoga. For now, it is enough to notice that Yoga is the main part of our app, which takes care of creating Express GraphQL server, using schema and resolvers we provide.

We use Prisma-binding to connect our database with our server. Schema for Prisma-binding is auto generated once we deploy our app to Prisma. We use the functions provided by Prisma-binding to communicate with our database and pass them through Context to our resolvers.

Resolvers must follow the same structure our schema does. We must define resolvers for every Type, Query and Mutation we define in our schema.graphql file, so that our server knows how to handle the data.

I hope I have given you a more detailed explanation on how things work and you figured some new things out. Please do comment on things you wish were better explained, so I can better explain them in my next posts. Also, follow me to get notified about new tutorials — I hope there will be plenty.

Cheers! 😄 👋