Mar 1, 2024

Beginner's guide to graphql

GraphQl was built by facebook as an alternative to REST, mainly to solve the problem of over fetchiing and under fetching.

Both of these are very common problems with REST APIs. You generally end up wasting bandwith or time.

GraphQL is a powerful query language for APIs that provides a more efficient, flexible, and powerful alternative to REST.

Funny analogy - Imagine you are at a restaurant. With REST, you order fixed combo meals. With GraphQL, its like ordering a la carte - you specify exactly what you want.

You can view the original graphql package here - https://www.npmjs.com/package/graphql

Key concepts

  1. Schema: The schema defines the types of data that can be queried and the relationships between them. It's like a blueprint for your data. ( like a menu in restaurant )

Basic schema example -

type Query {
hello: String
user(id: ID!): User
}

type User {
id: ID!
name: String!
email: String!
age: Int
}
type Query {
hello: String
user(id: ID!): User
}

type User {
id: ID!
name: String!
email: String!
age: Int
}
  1. Queries: Queries are used to fetch data from the server. They resemble a question you ask the server. ( like asking for a dish ) ( GET in REST)

Sample Query -

query {
hello
user(id: "1") {
name
email
}
}
query {
hello
user(id: "1") {
name
email
}
}

This is a query to fetch a user with id 1 and returns their name and email. It also fetches the string in the hello field.

If a client sends above query, the server will respond with the following JSON:

{
"data": {
"hello": "Hello world!",
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
}
{
"data": {
"hello": "Hello world!",
"user": {
"name": "Alice",
"email": "alice@example.com"
}
}
}
  1. Mutations: Mutations are used to modify data on the server. They resemble a command you give to the server. ( like ordering a dish ) ( POST / PUT / DELETE in REST)
  2. Subscriptions: Subscriptions allow you to receive real-time updates from the server.
  3. Types: Types define the structure of your data. They can be scalars (like strings, numbers, etc.) or objects with fields. ( like a menu item and its ingrediants )
  4. Resolvers: Resolvers are functions that define how to fetch data for a particular field in your schema. ( like a chef who prepares the dish )

How does GraphQL work ?

  1. Query: You send a GraphQL query to the server. This query specifies the data you want.
  2. Resolver: The server receives the query and uses the resolver functions to fetch the data. Note that the resolver function is responsible for fetching data from the database or any other data source.
  3. Response: The server sends back a response containing the requested data.

Implementing GraphQL Server

We will be using Apollo Server to implement a GraphQL server.

  1. Let setup a basic nodejs project. Run following commands in terminal one by one.
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql // install dependeencies
touch index.js
mkdir graphql-demo
cd graphql-demo
npm init -y
npm install apollo-server graphql // install dependeencies
touch index.js
  1. Update the package.json file with the start command -
{
// ...etc.
"type": "module",
"scripts": {
"start": "node index.js"
}
// other dependencies
}
{
// ...etc.
"type": "module",
"scripts": {
"start": "node index.js"
}
// other dependencies
}
  1. Open the index.js and import appllo server and startStandaloneServer from apollo server.
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
  1. First we will add the type definations.
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against
// your data.
const typeDefs = `#graphql
# Comments in GraphQL strings (such as this one) start with the hash (#) symbol.

# This "Book" type defines the queryable fields for every book in our data source.
type Book {
title: String
author: String
}

# The "Query" type is special: it lists all of the available queries that
# clients can execute, along with the return type for each. In this
# case, the "books" query returns an array of zero or more Books (defined above).
type Query {
books: [Book]
}
`
// A schema is a collection of type definitions (hence "typeDefs")
// that together define the "shape" of queries that are executed against
// your data.
const typeDefs = `#graphql
# Comments in GraphQL strings (such as this one) start with the hash (#) symbol.

# This "Book" type defines the queryable fields for every book in our data source.
type Book {
title: String
author: String
}

# The "Query" type is special: it lists all of the available queries that
# clients can execute, along with the return type for each. In this
# case, the "books" query returns an array of zero or more Books (defined above).
type Query {
books: [Book]
}
`
  1. Now lets add some sample dataset
// dataset
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
// dataset
const books = [
{
title: 'The Awakening',
author: 'Kate Chopin',
},
{
title: 'City of Glass',
author: 'Paul Auster',
},
];
  1. Now we will add the resolvers.
// resolvers
// Resolvers define how to fetch the types defined in your schema.
// This resolver retrieves books from the "books" array above.
const resolvers = {
Query: {
books: () => books,
},
};
// resolvers
// Resolvers define how to fetch the types defined in your schema.
// This resolver retrieves books from the "books" array above.
const resolvers = {
Query: {
books: () => books,
},
};
  1. Now we will add the server and start it.
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
});

// Passing an ApolloServer instance to the startStandaloneServer function:
// 1. creates an Express app
// 2. installs your ApolloServer instance as middleware
// 3. prepares your app to handle incoming requests
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});

console.log("🚀 Server ready");
// The ApolloServer constructor requires two parameters: your schema
// definition and your set of resolvers.
const server = new ApolloServer({
typeDefs,
resolvers,
});

// Passing an ApolloServer instance to the startStandaloneServer function:
// 1. creates an Express app
// 2. installs your ApolloServer instance as middleware
// 3. prepares your app to handle incoming requests
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});

console.log("🚀 Server ready");
  1. Now run the server using "npm start"

  2. Now we will test the server using graphql playground. Open http://localhost:4000/graphql in your browser.

  3. Now we will add the queries. Update the query to fetch all books.

query FetchAllBooks {
books {
title
author
}
}
query FetchAllBooks {
books {
title
author
}
}
  1. Run the query and you will see the result. Heres a screenshot of the result. You should see something similar to this.

Enhancing our graphql server

Let's say now you want to support additional query to fetch a single book by id. Here are the steps that you need to follow.

  1. Add the type definition for the new query.

Replace the following code to the typeDefs variable. Notice the type for new query

type Query {
books: [Book]
book(title: String!): Book # New query to fetch by title
}
type Query {
books: [Book]
book(title: String!): Book # New query to fetch by title
}
  1. Update the resolver to handle the query.
const resolvers = {
Query: {
books: () => books,

// New book resolver
book: (parent, args) => {
const { title } = args;

// Find book by title (case-sensitive exact match)
const foundBook = books.find(book =>
book.title === title
);

if (!foundBook) {
throw new Error("Book not found");
}

return foundBook;
}
},
};
const resolvers = {
Query: {
books: () => books,

// New book resolver
book: (parent, args) => {
const { title } = args;

// Find book by title (case-sensitive exact match)
const foundBook = books.find(book =>
book.title === title
);

if (!foundBook) {
throw new Error("Book not found");
}

return foundBook;
}
},
};
  1. Restart the server and test the new query.

Consuming GraphQL API

Lets try to consume the graphql api through postman. Import followign curl command in postman and send the request to your local graphql server.

curl -X POST -H "Content-Type: application/json" \
-d '{"query":"{ books { title author } }"}' \
http://localhost:4000 | jq .
curl -X POST -H "Content-Type: application/json" \
-d '{"query":"{ books { title author } }"}' \
http://localhost:4000 | jq .

Let's try to fetch a single book by title.

curl -X POST -H "Content-Type: application/json" \
-d '{"query":"query GetBook($title: String!) { book(title: $title) { title author } }", "variables": {"title": "1984"}}' \
http://localhost:4000 | jq .
curl -X POST -H "Content-Type: application/json" \
-d '{"query":"query GetBook($title: String!) { book(title: $title) { title author } }", "variables": {"title": "1984"}}' \
http://localhost:4000 | jq .

Notice how we are passing the graphql variables.

Why graphql calls use POST method ?

You might think , why are we using POST method to call the graphql api instead of GET method. Lets understand the reason.

Lets assume for a moment we want to use the GET method. We will need to pass the query as a query parameter.

(1) One of the problem with this approach is that the query parameter will be visible in the url. This is a security concern. E.g. if you are fetching a book by author, the query string would include the author name. This is a security concern as name is PII information

(2) Secondly, as the query becomes complex, the url will become very long. For all practical purposes, there are limitations put on the length of the url at different levels. First the browser these days only support upto 2000 characters in url. The proxy servers and the web servers also have limits on the length of the url.

(3) GET calls are expected to be idempotent. This means that calling the same GET request multiple times should return the same result and have NO SIDE EFFECTS. But in case of graphql, the query can be complex and can have side effects. So, using POST saves us from falsly implying that the query is idempotent.

(4) All query parameters are of type string. So using GET method, might force our graph ql severs to parse and translate the query parameters to the appropriate data types. This is not a issue with POST method. POST allows us to send different type of data in request body.

When to not use graphql ?

  1. Simple fixed data requirments
  2. You need low latency for response. Graphql is not good for high throughput applications as it can overload the server with complex queries.
  3. File uploads / streaming use cases.