GraphQL Part 1 – A Modern Approach to APIs

For years, REST (Representational State Transfer) has been the standard for web services. However, as applications grow in complexity, developers often find themselves juggling dozens of endpoints and dealing with over-fetching data.

GraphQL is a query language for your API and a server-side runtime for executing those queries using a type system you define for your data. Instead of multiple “dumb” endpoints, GraphQL provides a single “smart” endpoint that can return exactly what the client asks for.


Why GraphQL?

  • No More Over-fetching: You get exactly the data you request—nothing more, nothing less.
  • Single Request, Multiple Resources: You can fetch data from different sources in one trip to the server.
  • Strongly Typed: GraphQL uses a schema to define what is possible, which acts as a contract between the frontend and backend.
  • Self-Documenting: Because of the schema, tools like GraphiQL allow you to browse the API structure effortlessly.

For this tutorial we will be working with a list of classic Hammer Studios films. Each film will have id, title, year and watched fields.

The code below shows example Schema and the Query definitions. The schema defines a Film as having four fields. Where the field type is suffixed with “!” it indicates that the field must not be null and will always return a value.

const typeDefs = gql`
  type Film {
    id: ID
    title: String!
    year: Int
    watched: Boolean
  }
  input FilmFilter {
    year_gte: Int
    year_lte: Int
  }
  type Query {
    # Return a list of films, optionally filtered by watched status, year, or search term in the title
    films(watched: Boolean, year: Int, searchTerm: String, where: FilmFilter): [Film]
    film(id: ID!): Film
  }
`;

After the Schema we have queries defined. If you define a query without any parameters e.g. films: [Film] and try to use a parameter in your query GraphQL will complain…loudly with a GRAPHQL_VALIDATION_FAILED error.

Here we have defined two queries.

  1. films(watched: Boolean, year: Int, searchTerm: String, where: FilmFilter): [Film] – return a list of Film objects. We can optionally have zero or all of the following query parameters – watched, year, searchTerm, where(this is used to support range queries on the year field)
  2. film(id: ID!) – return a single Film. The id parameter MUST be specified

Getting Started: A Simple Implementation

I have created a Github repo for this tutorial. It is straightforward to follow. Once you have cloned the repository, open readme.md for instructions. Alternatively read on!

git clone https://github.com/jmwollny/lab.git
cd lab/graphql-tutorial
npm install

Once the dependencies have been installed you can run the Apollo server.

node index.js

You may be thinking, okay I’ve defined the Schema and the Queries, where do I get the data from and how do I map the queries to the underlying datasource?

The list of films is a hard-coded array defined in index.js. In practice we would be calling out to one or more data sources to get this information.

To map and filter the queries, this is where resolvers come in.

Open a terminal

cd lab/qraphql-tutorial

open index.js. This file contains the Schema, Queries and Resolvers and starts the Apollo server. In a production environment these would be split out into different files. We are using a single file to keep things simple.

At the bottom this file you will see the resolvers definition. Inside the films arrow function we can create filters for each of our defined query parameters.

To filter the dataset we check for the presence of the query parameter and perform the filter using the built-in Javascript filter function. We make sure to use the filtered list in any filters that follow.

When we are done we just return the list to the server.

const resolvers = {
  Query: {
    films: (parent, args) => {
      let filteredFilms = films;
      // Watch filter
      if (args.watched !== undefined) {
        filteredFilms = filteredFilms.filter(f => f.watched ===    args.watched);
      }   
      // Year filter
      if (args.year) {
        filteredFilms = filteredFilms.filter(f => f.year === args.year);
      }
      // Date range filter
      if (args.where) {
        if (args.where.year_gte) {
          filteredFilms = filteredFilms.filter(f => f.year >= args.where.year_gte);
        }
        if (args.where.year_lte) {
          filteredFilms = filteredFilms.filter(f => f.year <= args.where.year_lte);
        }
      }

      // Search filter
      if (args.searchTerm) {
        filteredFilms = filteredFilms.filter(f => 
          f.title.toLowerCase().includes(args.searchTerm.toLowerCase())
        );
      }
      
      return filteredFilms;
    },
    film: (parent, args) => films.find(f => f.id === args.id),
  },
};

GraphQL queries

For those used to SQL these may look a little odd at first but they are quite straightforward once you get the hang of of the syntax.

A simple query

Let’s retrieve the full list of films. Open your browser at http://localhost:4000/ then click the Query your Server button. If all is well the sandbox will open. Paste the following query.

query GetAllFilms {
  films {
    title
    watched
    year
   }
}

GetAllFilms is the name we give to our query. It can be anything that succinctly describes our query! Next we indicate that we want to execute the films query and return a list with title, watched and year fields. Note: you need to supply at least one field to be returned in the output.

Well done, you have succesfully run your first GraphQL query 🙂 This is what is returned.

Using a filter in our query

Say we wanted all films containing the word “dracula” that were made in the 1970s and that we haven’t watched.

In order to specify a range we need to define some extra variables to support the query. In our case we need year_gte and year_lte to define our bounds.

input FilmFilter {
  year_gte: Int
  year_lte: Int
}

We then define a where query parameter that uses the FilmFilter

films(watched: Boolean, year: Int, searchTerm: String, where: FilmFilter): [Film]

The last piece of the puzzle is to update our resolver to use our new variables.

// Date range filter
if (args.where) {
  if (args.where.year_gte) {
    filteredFilms = filteredFilms.filter(f => f.year>=args.where.year_gte);
  }
  if (args.where.year_lte) {
    filteredFilms = filteredFilms.filter(f => f.year <= args.where.year_lte);
  }
}

Finally we craft our query using the new where parameter.

query GetSeventiesDracula {
  films(
    where: { year_gte: 1970, year_lte: 1979 }, 
    searchTerm: "dracula", 
    watched: true) {
      id
      title
      year
      watched
  }
}

A best practice when it comes to GraphQL is to separate the query data from the query itself. This is accomplished using variables. In the sandbox the variables JSON can be entered in the area underneath the query text box. Our new query will look like this.

query GetSeventiesDraculaWithVars($where: FilmFilter, $searchTerm: String, $watched: Boolean) {
  films(where: $where, searchTerm: $searchTerm, watched: $watched) {
    id
    title
    year
    watched
  }
}

After the query name we pass in the list of variables that we will provide values for, along with their type. $where: FilmFilter, $searchTerm: String, $watched: Boolean). In the film query instead of declaring the values we provide placeholders prefixed by ‘$’.

All that remains is to provide the query with solid values. The JSON will look like this.

{
  "where": {
    "year_gte": 1970,
    "year_lte": 1979
  },
  "searchTerm": "dracula",
  "watched": true
}

Alternatively you can open a terminal session and use the curl command as shown below.

curl -X POST http://localhost:4000/ \
-H "Content-Type: application/json" \
-d '{
  "query": "query GetFilteredFilms($where: FilmFilter, $searchTerm: String, $watched: Boolean) { films(where: $where, searchTerm: $searchTerm, watched: $watched) { id title year watched } }",
  "variables": {
    "where": {
      "year_gte": 1970,
      "year_lte": 1979
    },
    "searchTerm": "dracula",
    "watched": true
  }
}'

Query results

{"data":{"films":[{"id":"133","title":"Dracula A.D. 1972","year":1972,"watched":true},{"id":"142","title":"The Satanic Rites of Dracula","year":1973,"watched":true}]}}

When to Use GraphQL

While GraphQL is powerful, it isn’t always the “REST-killer.”

Use GraphQL When…Use REST When…
You have complex, nested data requirements.Your app is simple with few resources.
You support multiple clients (Web, iOS, Android) with different data needs.You need standard HTTP caching mechanisms.
You want to aggregate data from multiple microservices.You are building a very small, lightweight microservice.

Final Thoughts

GraphQL shifts the power from the server to the client. By allowing the frontend to dictate the data structure, it speeds up development cycles and reduces the payload sent over the wire. Once you get to grips with the extra boilerplate and query syntax, it is surprisingly easy to use.

You may now be asking, “well, this is all well and good, but how do I update or delete records from the database?” Well, dear reader that is the topic for my next article.

Leave a Reply

Your email address will not be published. Required fields are marked *