DEV Community

loading...
Cover image for Super Powered APIs With GraphQL

Super Powered APIs With GraphQL

Kinanee Samson
I am a frontend web designer based in Nigeria, I am a determined and sarcastic person with a wicked sense of humor.
・7 min read

GraphQL, is a technology for fetching and mutating data that makes you wonder why you were building your servers to be REST endpoints for so long. In case you are just meeting it for the first time, grapQL is a runtime and query language that we can use to describe a format for our data and how to get that data. GraphQL is not tied to any specific programming language or database and as such it can be used with any database or language of your choosing so you don't need to learn anything from scratch. GraphQL is just a technology that bridges different parts of your application, this image can give you a hint of what i mean.

Alt Text

You can watch this short video to understand more about graphQL For this article i'll talk about how to create a and configure a basic node js GraphQL endpoint that we can make queries to. I'll be using typescript with nodejs on the server, you can find the tsconfig and package.json files here. If you are superhero developer you have gotten the above files, do store them inside a folder that will serve as the project's directory. Open up that directory in your text editor and let's dive in...

Index

If you have that project set up you can run npm install to get the dependencies for the project. Doing it manually you'd had to first;

  • Run npm i graphql apollo-server to install apollo-server and graphql for us.

  • Then we'd install TypeScript and nodemon npm i -D typescript nodemon.

  • change the main script in package.json to point to our js file "main": "dist/index.js",

  • Add the following to our scripts object still inside the package.json file "server": "nodemon dist/index.js".

  • Generate a tsconfig.json file using tsc --init and ensure it looks like this;

  {
    "compilerOptions": {
      "module": "commonjs",
      "esModuleInterop": true,
      "allowSyntheticDefaultImports": true,
      "target": "es2016", // or newer if your node.js version supports this
      // "strictNullChecks": true,
      "strictFunctionTypes": true,
      "noImplicitThis": true,
      "moduleResolution": "node",
      "strictNullChecks": false,
      "resolveJsonModule": true,
      "noUnusedLocals": true,
      "noUnusedParameters": true,
      "noImplicitReturns": true,
      "skipLibCheck": true,
      "declaration": false,
      "noFallthroughCasesInSwitch": true,
      "composite": false,
      "noImplicitAny": true,
      "lib": [
        "dom",
        "es2016",
        "esnext.asynciterable"
      ],
      "sourceMap": true,
      "emitDecoratorMetadata": true,
      "strict": false,
      "experimentalDecorators": true,
      "outDir": "dist",
      "rootDir": "src",
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Create a folder inside a directory like so /scr/index.ts. Ensure that /scr sits at the root level

Inside the the index.ts we will import ApolloServer and gql, gql will help us to compose our schema, define our queries and mutations. ApolloServer will allow us create an instance of an apollo server that we can make graphQL queries to. Open up the /src/index.ts and let's code up;

I'll be using firestore for the database, I already have a firebase project set up I will be using the admin sdk. I won't go into setting up a firebase project here because that would take the wind out of our sails.

//index.ts

//  * Importing our firebase-admin
import admin from 'firebase-admin'

// * Importing our serviceAccounnt
import serviceAccount from './serviceAccount.json'

// * Importing our apollo-server

import { ApolloServer, gql, ApolloError } from 'apollo-server'

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
  databaseURL: "firestore Database url"
});

const db = admin.firestore()
Enter fullscreen mode Exit fullscreen mode

Let's familiarize ourselves with what a schema is before we go into creating one.

A Schema is just what it is, graphQL is a strongly typed language like TypeScript. You can compose types from the built in graphQL types that will form a representation of your data. The language is flexible enough to let you to define relationship between your schemas which will enable you build complex schema for your data. Let's define a basic schema for an User and a channel, this will give us a basic intro and understanding of how graphQL schema works.

//  /src/index.ts

// Skipped

const typeDefs = gql`
"This will provide information about what you want to describe e.g a User"
# graphQL treats anything that begins with a # as a comment

"An User Schema another comment"

type User{
  email: String!,
  id: ID!,
  name: String!,
  channels: [Channel!]!
# A user can have many channels 
}

"Describes what a channel looks like"

type Channel {
  animal: String!,
  id: ID!,
  photoUrl: String!,
  title: String!
}
`
Enter fullscreen mode Exit fullscreen mode

We have defined a basic Schema for both an User and a Book. Let's take time to pick apart the above schema and see what there is to it. The type User refers to an Object Type which is one of the graphQL built in types this type allows us to build custom schemas. Some other built in graphQL types include (String, Int, Float, Boolean and ID) they are known as

Scalar
types, the User has an email and name property which are strings. The User has an id property which is of type ID, this is type that specifies a unique identifier, then there's a channel field which is an array of Channel, another type we defined. The exclamation marks are just there to ensure that graphQL doesn't return null to us. The Channel type has it's own schema which tells graphQL how to figure out the structure of what a channel looks like this will be useful when we will compose our queries. This is what makes graphQL so cool, when we query for the User, and we want to get his channels, we also get access to the properties on the Channel Schema, so we can return an array of only the channel names, and or more properties on each channel.

Queries

Inside our typeDefs we will define an additional type called Query, this is an object that defines the queries we can makes based on the types we defined above, let's see a code example;

// src/index.ts

// skipped

const typeDefs = gql`
type User {
  // Skipped 
}

type Channel {
  // Skipped
}

type Query {
  user(id: String!): User,  // We can query a user by their id
  users: [User!]!,          // We can query for all the users
  channels: [Channel!]!     // We can query for all the channels
}`

Enter fullscreen mode Exit fullscreen mode

When you make queries to this endpoint you can get;

  • A single user with their id.
  • A list of all the users, this will return to us an array of Users.
  • A list of all the channels, will return an array of Channels.

Resolvers

We are primarily done with our type definitions, let's look at resolvers. It is inside the resolvers that we get the actual data and map them to the types in our type definition. Let's dive in; You can resolve an entire type or you can just a property on the type, in this case we will resolve only the user, how to fetch a user based on their ID and how to fetch the list of channels that a user belongs to. We will also resolve the list of channels, the takeaway is that you can resolve your types but you must resolve your queries.

//  src/index.ts

// src/index.ts

// skipped

const typeDefs = gql`
type User {
  // Skipped 
}

type Channel {
  // Skipped
}

type Query {
  // Skipped
}`

const resolvers = {
    // Let's resolve the channels list on the user
    User { 
    // we can customize the atrribute or logic for getting each field on the types
    // we defined above, in this case we are only interested in the channels
    async channels (parent:any) {
       // the parent refers to an individual instance of a user
      // Get a reference to the channels collection
      const chanRef = await db.collection('channels').get()
      const channels = chanRef.docs.map(d => d.data() )
      // create an empty array
      const userChan:any[] = []
      // loop through the user's channels id 
      parent.channels.forEach((chan:any) => {
        // search the channels collection for the channel with an id that 
        //  matches the id we are iterating over
        const channel = channels.find((item:any) => chan == item.id)
        // add that chanel to the array of users channel
        userChan.push(channel)
      })
      return userChan
    }
  }, 
  // Let's resolve our Query
  Query: {
      // remeber the Query we defined in typeDefs, this is for a list of channels
    channels: async (parent, args) => {
        // Basic firebase
      const channelsRef = await db.collection('channels').get()
      return channelsRef.docs.map(c => c.data())
    },
    // this is for a list of users
    users: async (parent, args, context) => {
      try{
        // Basic firebase stuff
        const usersRef = await db.collection('users').get()
        return usersRef.docs.map(user => user.data())
      }
      catch(err) {
        console.log(err)
        return new ApolloError(err)
      }
    },
    // an individual user, when we want to query for a user, we can pass in 
    // an id as an argument, it will be added to args object but we are destructuring
    user: async (parent:any, {id}: any, context: any) => {
        // Basic firebase
      const userRef = await db.collection('users').doc(id).get()
      return userRef.data()
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Launching our Apollo Server

Now we just need to launch our server, to do that we create a new instance of an ApolloServer and pass it an object that contains the typeDefs and resolvers we defined above. We then call listen on the server like we would on an express server. Don't forget to compile it to JavaScript since we are using TypeScript for this project.

//  src/index.ts

// src/index.ts

// skipped

const typeDefs = gql`
type User {
  // Skipped 
}

type Channel {
  // Skipped
}

type Query {
  // Skipped
}`

const resolvers = {
    // Let's resolve the channels list on the user
    User { 
    // Skipped
  }, 
  // Let's resolve our Query
  Query: {
      // skipped
  }
}

const server = new ApolloServer({ typeDefs, resolvers })

server.listen().then(({ url }) => {
  console.log(`Server running on ${url}`)
})
Enter fullscreen mode Exit fullscreen mode

And that's our simple graphQL server set up, you can install the graphQL playground extension on chrome, copy the url from your terminal and paste in the url and test your Schema, just write your Schema inside and run it;

// example playground to test api

Query {
    users {
        name,
        email,
        channels {
            title,
            animal
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the query, you should see a list of users with channels that they are subscribed to. We will touch up mutations later, hope this gets you on your way to graphQL server. If all of this felt like junk then try watching video by Jeff on building an Apollo Server and I have to admit, it was the inspiration for this post. That being said i hope you found this useful and learned something from this.

Discussion (0)