Building a Client For Your GraphQL API

Building a Client For Your GraphQL API

Now that we have an understanding of GraphQL queries and API setup, it's time to get that data.

    If you had the pleasure of joining us last time, we had just completed a crash course in structuring GraphQL Queries. As much we all love studying abstract queries within the confines of a playground environment, the only real way to learn anything to overzealously attempt to build something way out of our skill level. Thus, we're going to shift gears and actually make something with all the dry technical knowledge we've accumulated so far. Hooray!

    Data Gone Wild: Exposing Your GraphQL Endpoint

    If you're following along with Prisma as your GraphQL service, the endpoint for your API defaults to [your_ip_address]:4466. What's more, you've probably noticed it is publicly accessible. THIS IS VERY BAD. Your server has full read/write access to whichever database you configured with it... if anybody finds your endpoint hanging out in a Github commit somewhere, you've just lost your database and everything in it. You're pretty much Equifax, and you should feel bad.

    Prisma has a straightforward solution. While SSHed into wherever-you-set-up-your-server, check out the prisma.yaml file which was generated as a result of when we first started getting set up. You know, this directory:

    my-prisma
    ├── datamodel.prisma
    ├── docker-compose.yml
    ├── generated
    │   └── prisma-client
    │       ├── index.ts
    │       └── prisma-schema.ts
    └── prisma.yml
    

    prisma.yaml seems inglorious, but that's because it's hiding a secret; or should I say, it's not hiding a secret! Hah!... (you know, like, credentials). Anyway.

    In order to enable authorization on our endpoint, we need to add a secret to our prisma.yaml file. The secret can be anything you like; this is simply a string which will be used to generate a token. Add a line which defines secret like this:

    endpoint: http://localhost:4466
    datamodel: datamodel.prisma
    secret: HIIHGUTFTUY$VK$G$YI&TUYCUY$DT$
    
    generate:
      - generator: typescript-client
        output: ./generated/prisma-client/
    

    With your secret stashed away safely, the Prisma CLI can now use this secret to create the authentication token. This will be the value we pass in the headers of our requests to actually interact with our Prisma server remotely.

    Type $ prisma token in your project directory to get the work of art:

    $ prisma token
    eyJhbGciOiJIUzI1NiIsInUYGFUJGSFKHFGSJFKSFJKSFGJdfSwiaWF0IjoxNTUyMTYwMDQ5LCJleHAiOjE1NTI3NjQ4NDl9.xrubUg_dRc93bqqR4f6jGt-KvQRS2Xq6lRi0a0uw-C0
    

    Nice; believe it or not, that was the "hard" part.

    EXTRA CREDIT: Assign a DNS Record and Apply a Security Certificate

    If really want to, you could already query against your insecure IP address and start receiving some information. That said, making HTTP requests as such from HTTPS origins will fail. Not only that, but you kind of look shitty for not even bothering to name your API, much less apply a free SSL certificate. For the easiest possible way to do this, see our post on using Caddy as an HTTP server.

    Building a Javascript Client to Consume Our API

    With our API nice and secure, we can start hitting this baby from wherever we want... as long as it's a Node app. We'll start by requiring two packages:

    const { GraphQLClient } = require('graphql-request')
    const { dotenv } = require('dotenv').config()
    

    GraphQLClient is the magic behind our client- it's everything. It also happens to be very similar to existing npm libraries for making requests, such as node-fetch.

    We'll also leverage the dotenv library to make sure our API endpoint and Bearer token stay out of source code. Try not to be Equifax whenever possible. dotenv allows us to load sensitive values from a .env file. Just in case you need a refresher, that file should look like this:

    NODE_ENV=Production
    ENDPOINT=https://yourapiendpoint.com
    AUTH=Bearer eyJhbGciOBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHGUYFIERIBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHBLAHZl-UGnMrOk3w

    Initialize The GraphQL Client

    I like to set up a one-time client for our API that we can go back and reuse if need be. After pulling the API endpoint and token from our .env file, setting up the client is easy:

    const { GraphQLClient } = require('graphql-request')
    const { dotenv } = require('dotenv').config()
    
    const endpoint = process.env.ENDPOINT;
    const token = process.env.AUTH;
    
    // Initialize GraphQL Client
    const client = new GraphQLClient(endpoint, {
      headers: {
        Authorization: token
      }
    });
    

    EMERGENCY MEETING: EVERYBODY HUDDLE UP

    Oh I'm sorry, were you focusing on development? Unfortunately for you, I spent 8 years as a product manager, and I love stopping everything suddenly to call emergency meetings.

    Real talk though, let's think back to the JIRA Kanban board example we've been using for the last two posts. If you recall, we're going to write a query that populates a 4-column Kanban board. The board represents a project (in this case, Hackers and Slackers) and each column represents a status of ticket, like this:

    const statuses = ['Backlog', 'To Do', 'In Progress', 'Done'];
    

    We've previously established that GraphQL queries are friendly to drop-in variables. Let's use this to build some logic into our client, as opposed to hardcoding a massive query, which is really just the same 4 queries stitched together. Here's what a query to populate a single JIRA column looks like:

    // Structured query
    const query = `
        query JiraIssuesByStatus($project: String, $status: String) {
             jiraIssues(where: {project: $project, status: $status}, 
             orderBy: timestamp_DESC, 
             first: 6) {
                key
                summary
                epic
                status
                project
                priority
                issuetype
                timestamp
                }
             }`

    We're passing both the project and the issue status as variables to our query. We can make things a bit dynamic here by looping through our statuses and executing this query four times: each time resulting in a callback filling the appropriate columns with JIRA issues.

    This approach is certainly less clunky and more dynamic than a hardcoded query. That said, this still isn't the best solution. Remember: the strength of GraphQL is the ability to get obscene amounts of data across complex relationships in a single call. The best approach here would probably be to build the query string itself dynamically using fragments, which we'll review in the next post.

    Game On: Our Client in Action

    const { GraphQLClient } = require('graphql-request')
    const { dotenv } = require('dotenv').config()
    
    const endpoint = process.env.ENDPOINT;
    const token = process.env.AUTH;
    
    // Initialize GraphQL Client
    const client = new GraphQLClient(endpoint, {
      headers: {
        Authorization: token
      }
    });
    
    // Structured query
    const query = `
       query JiraIssuesByStatus($project: String, $status: String) {
          jiraIssues(where: {project: $project, status: $status}, orderBy: timestamp_DESC, first: 6) {
             key
             summary
             epic
             status
             project
             priority
             issuetype
             timestamp
            }
          }
        `;
    
    // All Possible Issue Statuses
    const statuses = ['Backlog', 'To Do', 'In Progress', 'Done'];
    
    // Execute a query per issue status
    for(var i = 0; i < statuses.length; i++){
      var variables = {
        project: "Hackers and Slackers",
        status: statuses[i]
      }
    
      client.request(query, variables).then((data) => {
        console.log(data)
      }).catch(err => {
        console.log(err.response.errors) // GraphQL response errors
        console.log(err.response.data) // Response data if available
      });
    }
    

    Works like a charm. We only had one endpoint, only had to set one header, and didn't spend any time reading through hundreds of pages of documentation to figure out which combination of REST API endpoint, parameters, and methods actually get us what we want. It's almost as if we're writing SQL now, except... it looks a lot more like... NoSQL. Thanks for the inspiration, MongoDB! Hope that whole selling-open-source-software thing works out.

    Oh, and of course, here were the results of my query:

    { jiraIssues:
       [ { priority: 'Medium',
           timestamp: 1550194791,
           project: 'Hackers and Slackers',
           key: 'HACK-778',
           epic: 'Code snippets',
           status: 'Backlog',
           issuetype: 'Task',
           summary: 'HLJS: set indentation level' },
         { priority: 'Medium',
           timestamp: 1550194782,
    
           project: 'Hackers and Slackers',
           key: 'HACK-555',
           epic: 'Optimization',
           status: 'Backlog',
           issuetype: 'Task',
           summary: 'Minify Babel' },
         { priority: 'Medium',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-785',
           epic: 'New Post',
           status: 'Backlog',
           issuetype: 'Task',
           summary: 'Unix commands for data' },
         { priority: 'Medium',
           timestamp: 1550016000,
           project: 'Hackers and Slackers',
           key: 'HACK-251',
           epic: 'New Post',
           status: 'Backlog',
           issuetype: 'Content',
           summary: 'Using Ghost\'s content filtering' },
         { priority: 'Medium',
           timestamp: 1550016000,
           project: 'Hackers and Slackers',
           key: 'HACK-302',
           epic: 'Widgets',
           status: 'Backlog',
           issuetype: 'Integration',
           summary: 'Discord channel signups ' },
         { priority: 'Low',
           timestamp: 1550016000,
           project: 'Hackers and Slackers',
           key: 'HACK-336',
           epic: 'New Post',
           status: 'Backlog',
           issuetype: 'Content',
           summary: 'Linux: Configuring your server to send SMTP emails' } ] }
    { jiraIssues:
       [ { priority: 'Medium',
           timestamp: 1550224412,
           project: 'Hackers and Slackers',
           key: 'HACK-769',
           epic: 'Projects Page',
           status: 'Done',
           issuetype: 'Bug',
           summary: 'Fix projects dropdown' },
         { priority: 'High',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-710',
           epic: 'Lynx',
           status: 'Done',
           issuetype: 'Task',
           summary: 'Implement auto text synopsis for Lynx posts' },
         { priority: 'Medium',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-777',
           epic: 'Creative',
           status: 'Done',
           issuetype: 'Task',
           summary: 'Redesign footer to be informative; link-heavy' },
         { priority: 'Highest',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-779',
           epic: 'Urgent',
           status: 'Done',
           issuetype: 'Task',
           summary: 'Changeover from cloudinary to DO' },
         { priority: 'Medium',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-780',
           epic: 'Creative',
           status: 'Done',
           issuetype: 'Task',
           summary: 'Make mobile post title bold' },
         { priority: 'High',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-781',
           epic: 'Urgent',
           status: 'Done',
           issuetype: 'Bug',
           summary: 'This post consistently doesn’t work on mobile' } ] }
    { jiraIssues:
       [ { priority: 'Low',
           timestamp: 1550223282,
           project: 'Hackers and Slackers',
           key: 'HACK-782',
           epic: 'Widgets',
           status: 'To Do',
           issuetype: 'Task',
           summary:
            'Lynx: on mobile, instead of full link, show domainname.com/...' },
         { priority: 'High',
           timestamp: 1550194799,
           project: 'Hackers and Slackers',
           key: 'HACK-774',
           epic: 'Widgets',
           status: 'To Do',
           issuetype: 'Task',
           summary: 'New Widget: Next/Previous article in series' },
         { priority: 'Low',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-395',
           epic: 'Page Templates',
           status: 'To Do',
           issuetype: 'Task',
           summary: 'Create fallback image for posts with no image' },
         { priority: 'High',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-756',
           epic: 'Newsletter',
           status: 'To Do',
           issuetype: 'Major Functionality',
           summary: 'Automate newsletter' },
         { priority: 'Low',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-775',
           epic: 'Projects Page',
           status: 'To Do',
           issuetype: 'Data & Analytics',
           summary: 'Update issuetype icons' },
         { priority: 'Lowest',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-776',
           epic: 'Projects Page',
           status: 'To Do',
           issuetype: 'Task',
           summary: 'Add fork icon to repos' } ] }
    { jiraIssues:
       [ { priority: 'High',
           timestamp: 1550102400,
           project: 'Hackers and Slackers',
           key: 'HACK-784',
           epic: 'New Post',
           status: 'In Progress',
           issuetype: 'Content',
           summary: 'Welcome to SQL part1' } ] }
    

    Before we say "GG, 2ez, 1v1 me," know that we're only getting started uncovering what GraphQL can do. It's not all just creating and deleting records either; we're talking full-on database JOIN equivalent type shit here. Stick around folks, the bandwagon's just getting warmed up.

    Todd Birchard's' avatar
    New York City Website
    Engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.

    Engineer with an ongoing identity crisis. Breaks everything before learning best practices. Completely normal and emotionally stable.