Abdelrahman Awad
Software Engineer @Rasayel. Open-Source contributor.
In my opinion, it is superior in every way.
But it introduces its own problems and caveats.
So ... Everyone?
Addresses the following shortcomings of REST:
Typical REST response:
{
users: {
items: [
// useless user objects elements
],
pageInfo: {
totalCount: 150000,
currentPage: 1,
perPage: 100
}
}
}
👈I only needed this
{
"courses": [
{
"id": 1,
"name": "Intro to Software Engineering"
},
{
"id": 2,
"name": "Why you should stop using Angular"
},
{
"id": 3,
"name": "Refactoring UI"
}
]
}
{
"availability": [
{
"courseId": 1,
"available": false
},
{
"courseId": 2,
"available": true
},
{
"courseId": 3,
"available": false
}
]
}
Having to call more endpoints as more data is needed
Fun Tip: By solving overfetching in some scenarios, you might cause undefetching in others
Don't map your database to the GraphQL schema
type User {
_id: ID!
}
type User {
id: ID!
}
🤮
😎
Use nested queries when it makes sense
type Post {
user_id: ID!
}
type Post {
author: User!
}
🤮
😎
Use nested queries when it makes sense
type Query {
userPosts (userId: ID!, skip: Int, first: Int): [Post!]!
}
type User {
posts (first: Int, skip: Int): [Posts!]!
}
type Query {
user(id: ID!): User!
}
🤮
😎
Using scalars for type validation
Using directives for constraints
A lot of data constrains can be their own type
Not only you are making it clear what type of data your API accepts but also you are explicit about the consumer responsibilities
You can map custom scalars defined by the API to your own language's native type
For example the API could have a Timestamp that is sent as a `string` between requests but parsed as a native DateTime object on the client side.
Directives are like a middleware that can be applied on queries or fields.
type User {
secret: String! @can(ability: "SEE_SECRETS")
}
Use them when you have a "varying" constraint on a given query or a field.
input PostInput {
title: String! @min(length: 5)
}
type Mutation {
updateName(name: String): [ID!]!
}
Which of these are valid payloads 🤔
{
"name": null
}
{
"name": ""
}
{
}
✅
✅
✅
type Post {
id: ID!
title: String!
author: Author!
}
type Author {
id: ID!
name: String!
}
➡
query Posts {
posts {
author {
name
}
}
}
In Node.js (JavaScript) we have this thing called the event-loop, which we can abuse to batch the "calls" to the resolvers.
dataloader: https://github.com/graphql/dataloader
async function batchFunction(keys) {
const results = await db.fetchAllKeys(keys);
return keys.map(key => results[key] || new Error(`No result for ${key}`));
}
const loader = new DataLoader(batchFunction);
lighthouse-php: https://lighthouse-php.com/4.6/performance/n-plus-one.html
This isn't Node.js specific and can be done with any language.
type Post {
id: ID!
title: String!
author: Author!
}
type Author {
id: ID!
name: String!
posts: [Post!]!
}
➡
query Posts {
posts {
author {
posts {
author {
posts {
author {
posts {
# and so on
}
}
}
}
}
}
}
}
Just use a plugin or a library to limit the query depth:
type Query {
posts (skip: Int, first: Int): [Posts!]!
};
The classic
Slightly Advanced
type PageInfo {
perPage: Int!
totalPages: Int!
totalCount: Int!
hasPreviousPage: Boolean!
hasNextPage: Boolean!
}
type PostPagination {
items: [Posts!]!
pageInfo: PageInfo!
}
type Query {
posts (skip: Int, first: Int): PostPagination!
};
Non-Uniform Paging: Fetching pages of different lengths, useful to adapt to user internet speed or any arbitrary reason
Assume I fetch the first 10 items, then the next 5 items. What page am I on now? 🤔
Instead of using a `skip` and `limit` slicing, you could use an opaque cursor or a pagination id.
type PageInfo {
hasNext: Boolean!
hasPrevious: Boolean!
endCursor: String
startCursor: String
}
type PostPagination {
items: [Posts!]!
pageInfo: PageInfo!
}
type Query {
posts (after: String, before: String, first: Int): PostPagination!
};
Using Edges and Nodes to represent relational data
Nest Mutations
input AddBookInput {
ISBN: String!
title: String!
}
input RemoveBookInput {
bookId: Int!
}
input UpdateBookInput {
ISBN: String!
title: String!
}
type AuthorOps {
addBook(input: AddBookInput!): Int
removeBook(input: RemoveBookInput! ): Boolean
updateBook(input: UpdateBookInput!): Book
}
type Mutation {
Author(id: Int!): AuthorOps
}
type Mutation {
postUpdate (id: ID!, input: PostInput!): Post
}
Cute, but we have no way to know if the post was not found or if the update failed.
Is a failed login attempt really an exception?
Is an authentication failure an exception?
To-throw or not depends on the ability of frontend to recover from such exception
If the consumer of your API can recover from an error and that error is an expected possible outcome, don't throw!
interface MutationResponse {
code: String!
success: Boolean!
message: String!
}
type PostMutationResponse implements MutationResponse {
code: String!
success: Boolean!
message: String!
post: Post
}
type Mutation {
postUpdate (id: ID!, input: PostInput!): PostMutationResponse!
}
mutation UpdatePost ($id: ID!, $input: PostInput!) {
response: updatePost (id: $id, input: $input) {
success
code
message
node: post {
# Fields to patch the in-view item with
}
}
}
This convention makes mutations return the exact same structure. Your API consumer can build seamless mutation handling flows that work for any mutation.
Compare the following styles:
[action][object]
createUser
createPost
createProduct
updatePost
updateProduct
[object][action]
userCreate
userUpdate
postCreate
postUpdate
productCreate
seen in Product Hunt GraphQL API
GraphQL is a standard query language. Follow the specs whenever possible.
Status Codes: GraphQL spec doesn't care about transports (HTTP), status codes are meaningless and 200 is the same as 401
Uploading Files: GraphQL spec doesn't handle files, there is an RFC implementation.
By Abdelrahman Awad