Alright, it's been a bit longer than I was hoping since the latest entry in my series on GraphQL. Turns out 2 year old's don't really leave you alone on your computer to type things out too often.
I've had a lot of great response from the last posts, some of which has actually caused me to look in to other way of setting up a GraphQL server. Initially I was going to be using GraphQL Yoga to set up a server and using Apollo Client on the client side for the database queries and mutations. I'm actually now going to be using Apollo Server (v2) to get the GraphQL server set up! It's very similar to Yoga but given I am using Apollo Client I figured they would work better together. After a few days tinkering (everything takes a while when a 2 year old just wants more "rockets!" on Kerbal Space Program) I've finally got everything situated. I'll try and walk through my struggles/learnings getting everything set up correctly. This is going to be a pretty long post but I hope it can be somewhat definitive or helpful to those really just getting started.
GraphQL Servers - What's Happening
Before diving in to creating the server, I wanted to discuss the components needed for getting the server up and running. You essentially need:
- An instance of the Prisma Client.
- A 'typeDefs', or schema for the client side.
- Resolvers (for all queries and mutations)
- The actual GraphQL Server (Apollo Server 2)
What do these all do?
Prisma Client
The thing about Prisma is that it is essentially a server sitting on top of your actual database. To use it, you need to access it's "endpoint." (An explanation for a 5-year old). If you have gone through Part 2 of my series, you would already have this endpoint created for you by Prisma when you generated the Prisma Client and the prisma.graphql file. If you haven't gone through that already, you will probably get lost here.
Article No Longer Available
You can actually access your database through that Prisma playground directly, but we need a way for our application to talk to the same endpoint. I am doing this by creating a new instance of the Prisma Client using the JS constructor. This essentially creates the Prisma DB and allows you to access it in the server that we will be creating that our actual app backend runs on. In this series the endpoint is the Prisma demo database we made, but in production this would be your actual database.
typeDefs (schema.graphql)
I think this is where things got confusing for me initially. When setting up Prisma we defined the data model we wanted in our datamodel.prisma so that Prisma would go off and create our API for us to work with the database. The thing is, that datamodel and API will not be accessible to our client side. We need to define a new schema that will be accessible to our client side. In this client-side schema, we define the various queries and mutations that we are going to use in the actual application.
Resolvers
As mentioned in the previous parts of the series, Prisma creates this great API to access the database, but it has absolutely no "logic" build in. Here is an example of what that means:
Jane makes an account with her email jane@email.com. A year later, she comes back and tries to sign up again with the email jane@email.com (same email). The app should see she already has an account and say, "hey, just go log in!" but the Prisma API doesn't have that logic available. It can literally only make the user.
So that's what the resolvers take care of. We could make a resolver for creating a new user that would first use a query from the Prisma API to check if the user existed, and then either go ahead and create the user using a mutation from the API if they were in fact a new user, or do something else like prompt them to just sign in or reset their password.
The GraphQL Server
This is essentially your normal application server. It's just like creating a server in Express, the difference is that you give your instance of the server all of this extra GraphQL information such as the Prisma instance and all your resolvers so that your server knows how to communicate with your client-side code and the Prisma database.
Prepping the Schema and Resolvers
One nice thing about GraphQL is that it's all type safe. One frustrating thing about that can be if you are just getting set up you can get a ton of errors if you just try and start up the server without anything else created because the Apollo Server insists that things are good to go! Here we will set up each file our server needs, and lastly create the server.
1. File Structure
First, for these files that the server will be accessing I create a 'src' folder in the root of the project with the following 3 files.
prisma-graphql-tutorial
/src
/Mutation.js
/Query.js
/schema.graphql
2. schema.graphql
Below is the beginning of my schema.graphql, where I have 1 query set up to find a particular user, as well as 1 mutation that would create a new user.
# import * from "../generated/prisma.graphql"
type Query {
user(id: ID!): User
}
type Mutation {
createUser(
name: String!
): User!
}
First, I have the # import * from "../generated/prisma.graphql"
because that's how you import the types into the file from the Prisma API (prisma.graphql). Without it, you get these errors because when you have user(id: ID!): User
, which is saying you want to get a User returned to you after running user(id: ID!)
. Importing the User type helps avoid the errors.
So what's this all mean?
user(id: ID!): User
This creates a query that we will be able to use client-side called 'user' that accepts the unique user id as an argument. ID!
means that an id is required.
Where does this come from? It comes from the prisma.graphql that's generated by Prisma. If you head in to that file and seach for "Query" you will find all of the queries:
type Query {
item(where: ItemWhereUniqueInput!): Item
items(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Item]!
itemsConnection(where: ItemWhereInput, orderBy: ItemOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ItemConnection!
user(where: UserWhereUniqueInput!): User
users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
node(id: ID!): Node
}
You can see that there is a user(where: UserWhereUniqueInput!): User
. If you then search for the UserWhereUniqueInput!
you will find this gem:
input UserWhereUniqueInput {
id: ID
}
So this tells us that Prisma has made available to us a Query that's called user and it accepts the UserWhereUniqueInput, which is simply and id. That makes sense because we only want 1 user returned.
We can contrast that with the Query users which takes the UserWhereInput. If we search out what that is, we find this:
input UserWhereInput {
I deleted a lot of what was here to make this shorter...
id_gte: ID
id_contains: ID
id_not_contains: ID
name_lte: String
name_gt: String
name_gte: String
name_contains: String
name_not_contains: String
name_starts_with: String
}
So, if you wanted to query multiple users you can see that there are WAY more options available to pass into our "users" query. Whenever you want to find out what type of inputs we are supposed to be plugging away into our Prisma queries and mutations, you can just search for the actual input names.
To review -> I created a client-side user(id: ID!): User
Query, which I created by finding the actual Query within my prisma.graphql, and using that to find the inputs that the Prisma API would be expecting (the unique id).
createUser(name: String!): User!
Creating this followed a similar pattern to the Query I did above! First, I look the the Mutations in the prisma.graphql and find createUser(data: UserCreateInput!): User!
. This createUser needs some data, which is the UserCreateInput. In prisma.graphql we can look for UserCreateInput to see what that data is, and we find this:
input UserCreateInput {
name: String!
}
It's pretty basic, by way back when we defined what we wanted our User type to look like in the datamodel.prisma, all we said we wanted was a name and an id (but the id would get generated in the db).
So, for our mutation in our schema, we make createUser()
, and for the input we pass in name: String!
. The name is required, and after making a new user we want that User object returned to us (That's the : User!
part)
3. Mutation.js (Mutation Resolvers)
In this file, we will handle all the actual mutations. So far I have this:
const Mutations = {
async createUser(parent, args, context, info) {
const user = await context.db.mutation.createUser({
data: { ...args } // destructure arguments into the data (name, etc.)
}, info);
return user;
}
}
module.exports = Mutations;
In this example I am creating a mutation called createUser, and in Apollo (the resolvers are for Apollo Server) the function will take 4 arguments. These are the parent, arguments (args), context, and info. There is a bunch of great documentation on these here, I'm only going to explain them briefly.
- Parent: An object containing the results of the Apollo parent
- Args: The arguments passed in to the Apollo Client on the client side. Next post will be more about this...
- Context: An object available on all resolvers for each request.
- Info: Information on the execution state of the query.
What's happening is we are creating an async function where we await the result of context.db.mutation.createUser. That's the Prisma createUser in there! See it!
We have access to this first by accessing the context. Context is available on every single request. Actually, since this is all Express based, if you're familiar with Express, it's similar to the request, response. With context, context.request in the Apollo Server is identical to req in a normal Express server. In addition to the typical request data, we have access to our Prisma database through db which you will see get set up in the server later. Once you are in the Prisma db, we drill down to the mutations, and finally the actual mutation we want, which is createUser!
As we know from the API we have in our prisma.graphql file, createUser needs data, which we pass {...args}
. Any arguments would get destructured into appropriate key.value pairs based on the schema.graphql we just made, and the arguments would be...you guessed it, the name from createUser(name: String!): User!
.
At the end, we pass in info to the createUser and then return the user we created.
4. Query.js (Query Resolvers)
Ok, most of the heavy explaining was in the Mutation.js above, so here is my Query resolver:
const Query = {
user(parent, args, context, info) {
if (!context.request.userId) {
return null;
}
return context.db.query.user({
where: { id: context.request.userId }
}, info);
}
};
module.exports = Query;
Again you see the same (parent, args, context, info) function structure. Within the function I am first checking the request (context.request) for a userId. If there is a userId on the request object, we call the Prisma .user() method on our database passing context.request.userId as the id we are looking for.
Creating the GraphQL Server
Finally! Everything is in place to create our server. This is (arguably) the easy part. We'll tackle this in steps.
1. Create an index.js in your root folder.
2. Install the dependencies:
npm install apollo-server graphql graphql-cli graphql-import prisma prisma-binding
npm install --save-dev nodemon
3. Import everything you need into index.js
const { Prisma } = require('prisma-binding');
const { ApolloServer } = require('apollo-server');
const { importSchema } = require('graphql-import');
const typeDefs = importSchema('./src/schema.graphql');
const Query = require('./src/Query');
const Mutation = require('./src/Mutation');
The first 3 requires bring in Prisma, the Apollo Server, as well as the graphql-import package from Prisma that allows you to import the schema we made from a separate file and be read correctly.
We then import our schema (using importSchema) as typeDefs, as well as our Mutations and Query resolvers.
4. Create db using Prisma Client Constructor
const db = new Prisma({
typeDefs: './generated/prisma.graphql',
endpoint: 'https://us1.prisma.sh/prisma-tutorial/prisma-graphql-tutorial/dev',
secret: 'currentlyDrinkingPhilzCoffee'
});
We create a new instance of a Prisma Client. It needs it's own typeDefs, which for the Prisma Client is not the one we imported above, but the auto-generated ones from Prisma. It also needs the endpoint and secret that are the same as the ones from way back when in the prisma.yml. In non-tutorial land, you would want to put all that in a .env file and access from there, but it's there so you can see it.
5. Create the Server
Finally! We use ApolloServer to create a new server.
const server = new ApolloServer({
typeDefs,
resolvers: {
Mutation,
Query
},
context: ({ req }) => ({
...req,
db
})
})
A new ApolloServer takes the:
-
typeDefs: The ones we imported using
importSchema('./src/schema.graphql')
- resolvers: Object with the imported Query and Mutations resolvers
-
context: Super important! . Here, for the context we take every request (remember, this is all Express somewhere back there...) and return a new object that contains a copy of the request, along with
db
, which is the instance of our Prisma Client. This is how we are able to access Prisma in all of our resolvers usingcontext.db.[query/mutation].[method]
. It's because we are adding on the Prisma Client to each request right here.
6. Start the Server!
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
});
Similar to Express, you gotta tell the server to start listening.
Now you can nodemon index.js
and if all goes well, you'll see something like:
➜ prisma-graphql-tutorial (master) ✗ nodemon index.js
[nodemon] 1.18.10
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node index.js`
🚀 Server ready at http://localhost:4000/
You can then head over to localhost:4000 and you'll see a playground just like if you went to the Prisma endpoint. The difference is it's on your server and you can actually access it outside the playground!
7. Test It
You can do a quick test to see if all goes well by doing a simple query in the localhost playground.
Paste this into the playground, hit play:
mutation {
createUser(name: "Test Person") {
name
id
}
}
This runs the createUser mutation with the name of "Test Person", and requests back the name and id.
If all goes well you'll see this returned:
{
"data": {
"createUser": {
"name": "Test Person",
"id": "cjtdg3zu35pp70b51fjyj7vd1"
}
}
}
Congrats!
You've made it to the end of making a server. It's already a super long post, so all I'll say now is look forward to the future posts on how to use Apollo Client to make queries and mutations from the client side!
If you see anything wonky please let me know! This is literally my first Apollo Server ever, so it seems to work but let me know how it goes for you! If it helpful you can find this all on my git repository below.
Top comments (1)
Ryan,
These posts are really fantastic. They provide some of the most comprehensive information available(outside the official docs) on how to get full stack applications going with Prisma.
2 questions for you:
1) What was the reason behind deciding to use prisma-bindings over the newer prisma client api? Prisma Client vs Prisma Bindings
From this screenshot, it seems like Client is an evolution of Prisma bindings to a certain degree.
I noticed that the team started updated all their official docs to go from using Prisma bindings to Prisma Client. In all honesty, i'm not completely sure what the deep differences are between the two, other than that prisma client was supposedly meant to be more simplified/streamlined.
2) When you hook things up on the client side with React (assuming you're using React), how do you plan to handle your client side state management? Will you be reaching for something like Redux/Mobx, or will you try Apollo's built in cache store?
Again, thanks for the awesome tutorials!