GraphQL vs. REST API Case Study

If you’ve been watching this space, you’ve seen me talking about Nautobot’s GraphQL capabilities and how GraphQL helps you:

  • GraphQL queries are much more efficient than RESTful queries
  • GraphQL makes your life easier by making data more accessible
  • The above results in dramatic improvement in your quality of life

This post is a case study in those aspects. It will empirically demonstrate how GraphQL:

  • Minimizes your number of queries
  • Returns only the data you want
  • Makes it so you don’t have to manually filter data and build the desired data structure in a script
  • Creates faster automation
  • Reduces your workload

I will be running this case study using https://demo.nautobot.com/. Readers are encouraged to follow along, using the scripts below.

The Problem Statement

In this case study, the goal is to gather specific information for certain network elements from Nautobot. Specifically, we want a data structure with the following information:

  • We want information from all devices within the ams site
  • The data structure should organize information so that all the data is grouped on a per-device basis
  • We want this specific data for each device:
    • Device name
    • Device role
    • All the interface names
    • The list of IP address(es) for each interface, even if the interface has no configured IP address(es)

The GraphQL Solution

The GraphQL solution will leverage the pynautobot Python package, which provides a customized and efficient way to programmatically query Nautobot via GraphQL.

Also, from an earlier blog post in this GraphQL series, recall that you can craft GraphQL queries in Nautobot’s GraphiQL interface.

Here is the script we will use to accomplish our task using GraphQL:

import pynautobot

from pprint import pprint
from time import time

start_time = time()

# Nautobot URL and auth info
url = "https://demo.nautobot.com"
token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

# GraphQL query
query = """
query {
  devices(site:"ams") {
    name
    device_role {
      name
    }
    interfaces {
      name
        ip_addresses {
        address
      }
    }
  }
}
"""

print("Querying Nautobot via pynautobot.")
print()

print("url is: {}".format(url))
print()

print("query is:")
print(query)
print()

nb = pynautobot.api(url, token)

response = nb.graphql.query(query=query)
response_data = response.json

print("Here is the response data in json:")
pprint(response_data)
print()

end_time = time()

run_time = end_time - start_time

print("Run time = {}".format(run_time))

GraphQL Results

Here are the results when the script runs (snipped in places for brevity):

blogs/graphql_vs_restful % python3 -i graphql_query_ams_device_ints_pynautobot.py
Querying Nautobot via pynautobot.

url is: https://demo.nautobot.com

query is:

query {
  devices(site:"ams") {
    name
    device_role {
      name
    }
    interfaces {
      name
	  ip_addresses {
          address
      }
    }
  }
}


Here is the response data in json:
{'data': {'devices': [{'device_role': {'name': 'edge'},
                       'interfaces': [{'ip_addresses': [{'address': '10.11.192.0/32'}],
                                       'name': 'Ethernet1/1'},
                                      {'ip_addresses': [{'address': '10.11.192.2/32'}],
                                       'name': 'Ethernet2/1'},
                                      {'ip_addresses': [{'address': '10.11.192.4/32'}],
                                       'name': 'Ethernet3/1'},
                                      {'ip_addresses': [{'address': '10.11.192.8/32'}],
                                       'name': 'Ethernet4/1'},
                                      < --- snip for brevity --- >
                                      {'ip_addresses': [],
                                       'name': 'Ethernet60/1'},
                                      {'ip_addresses': [{'address': '10.11.128.1/32'}],
                                       'name': 'Loopback0'},
                                      {'ip_addresses': [],
                                       'name': 'Management1'}],
                       'name': 'ams-edge-01'},
                      {'device_role': {'name': 'edge'},
                       'interfaces': [{'ip_addresses': [{'address': '10.11.192.1/32'}],
                                       'name': 'Ethernet1/1'},
                                      < --- snip for brevity --- >
                                      {'ip_addresses': [{'address': '10.11.128.2/32'}],
                                       'name': 'Loopback0'},
                                      {'ip_addresses': [],
                                       'name': 'Management1'}],
                       'name': 'ams-edge-02'},
                      {'device_role': {'name': 'leaf'},
                       'interfaces': [{'ip_addresses': [{'address': '10.11.192.5/32'}],
                                       'name': 'Ethernet1'},
                                      {'ip_addresses': [], 'name': 'Ethernet3'},
                                      < --- snip for brevity --- >
                                      {'ip_addresses': [{'address': '10.11.64.0/32'}],
                                       'name': 'vlan99'},
                                      {'ip_addresses': [{'address': '10.11.0.0/32'}],
                                       'name': 'vlan1000'}],
                       'name': 'ams-leaf-01'},
                      {'device_role': {'name': 'leaf'},
                       'interfaces': [{'ip_addresses': [{'address': '10.11.192.9/32'}],
                                       'name': 'Ethernet1'},
                                      {'ip_addresses': [{'address': '10.11.192.11/32'}],
                                       'name': 'Ethernet2'},
                                      {'ip_addresses': [], 'name': 'Ethernet3'},
                                      < --- snip for brevity --- >                                      
                                      {'ip_addresses': [], 'name': 'Management1'},
                                      {'ip_addresses': [{'address': '10.11.1.0/32'}],
                                       'name': 'vlan1000'}],
                        < --- snip for brevity --- >
                      {'device_role': {'name': 'leaf'},
                       'interfaces': [{'ip_addresses': [{'address': '10.11.192.33/32'}],
                                       'name': 'Ethernet1'},
                                      < --- snip for brevity --- >
                                      {'ip_addresses': [{'address': '10.11.7.0/32'}],
                                       'name': 'vlan1000'}],
                       'name': 'ams-leaf-08'}]}}

Run time = 1.8981318473815918
>>> 

Take specific note of the following GraphQL features demonstrated above:

  • The returned data comes back in a structure that matches that of the query
  • GraphQL returns only the requested data
  • The returned data is ready for programmatic parsing

Running the script six times produced an average of 2.23 seconds, returning data for ten devices in the ams site.

RESTful Solution

For the RESTful solution, we’re not concerned about matching the exact data structure returned by GraphQL. We’re only concerned with getting the same data into a structure that can be parsed for programmatic use.

The GraphQL results were grouped by device, and the RESTful solution will do that as well, but will have some small format changes.

Here is the format for the data structure that the RESTful solution will return:

{ 
  <device_1_name>: {
    'role': <device_1_role>,
    'interface_info': {
      <interface_1_name>: [list of ip addresses for interface_1],
      <interface_2_name>: [list of ip addresses for interface_2],
        . . . 
    }
  }
  . . . 
  <device_n_name>: {
    'role': <device_n_role>,
    'interface_info': {
      <interface_1_name>: [list of ip addresses for interface_1],
      <interface_2_name>: [list of ip addresses for interface_2],
        . . . 
    }
  }
}

The format above is slightly different than that of the GraphQL results, but is still programmatically parsable.

The RESTful script that returns the data is below. When examining it, take note of the following:

  • We had to artificially construct the data structure, which required a non-trivial amount of work
  • The RESTful script requires three distinct API calls, with some calls iterated multiple times
  • Each API call returns WAY more information than we are interested in
  • Since the call to get interface data for the ams site returns so much extraneous information, Nautobot applies the default limit of 50 results per call
    • The limit constraint reduces the load on the Nautobot server
    • With the default database in https://demo.nautobot.com, the call to get all the interface data iterates six times, returning up to 50 results per call
  • The call to get the IP address information must iterate once for each of the ten devices in the ams site
  • The RESTful script is over twice as long as the GraphQL script and is much more complex
  • The amount of time required to construct, test, and validate the RESTful script was well over an order magnitude longer than that required for the GraphQL script (your mileage may vary!)
"""
Use REST API calls to get the following info for each device in 'ams' site:
- device name
- device role
- interface info
  - interface name
  - ip address

"""

import json
import requests

from pprint import pprint
from time import time

start_time = time()

# Looking at the Nautobot API:
#  - /api/dcim/devices gives you name and role
#  - /api/dcim/interfaces gives you all the interface info
#  - /api/addresses gets IP address info

# Define general request components
payload = {}
headers = {
    "Content-Type": "application/json",
    "Authorization": "Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}

##########################################

# Define devices url, query for 'ams' site devices
ams_dev_url = "https://demo.nautobot.com/api/dcim/devices?site=ams"

# Query Nautobot for the ams devices
ams_dev_resp = requests.get(ams_dev_url, headers=headers, data=payload)

# Turn the response text string into json
ams_devices_json = ams_dev_resp.json()

# Device info dict
device_info = {}

# Create a dict with device names as keys; the value for each key will be a dict.
for device in ams_devices_json["results"]:
    role = device['device_role']['display']
    dev_name = device["name"]
    device_info[dev_name] = {
        'role': role,
        'interface_info': {},
     }

print("device_info is:")
pprint(device_info)
print()
print()

##########################################

print("The GraphQL query returned all interfaces for a device, regardless of whether ")
print("an ip address was configured; we will match that here.")
print()

print("Gathering interface info for `ams` site.")

# Define url for device interfaces in 'ams' site
ams_interface_url = "https://demo.nautobot.com/api/dcim/interfaces?site=ams"

# Define a list to hold the interface info for `ams` site
ams_interface_info = []

# Account for ams_interface_url results limit; iterate url until 'next' url is None
while ams_interface_url is not None:
    ams_interface_resp = requests.get(ams_interface_url, headers=headers, data=payload)
    ams_interface_json = ams_interface_resp.json()
    ams_interface_url = ams_interface_json["next"]
    print("ams_interface_url is {}".format(ams_interface_url))
    ams_interface_info.extend(ams_interface_json["results"])
print()

print("Adding interface names to device_info for the appropriate device.")
# Filter out the interface names and add them in device_info
for interface_entry in ams_interface_info:
    dev_name = interface_entry["device"]["name"]
    interface_name = interface_entry["name"]
    device_info[dev_name]['interface_info'][interface_name] = []
print()

#####################################

print("Finally, gather the IP address info for each interface.")
print("This RESTful call returns only interfaces that have IP addresses configured.")
print()
ip_info_list = []

for device in device_info.keys():
    ip_url = "https://demo.nautobot.com/api/ipam/ip-addresses?device={}".format(device)

    # Account for ip_url results limit; iterate url until 'next' url is None
    while ip_url is not None:
        print("ip_url = {}".format(ip_url))
        ip_url_response = requests.get(ip_url, headers=headers, data=payload)
        ip_json = ip_url_response.json()
        ip_url = ip_json["next"]
        ip_info_list.extend(ip_json["results"])
print()

print("Add the IP address info to device_info.")
print()
for item in ip_info_list:
    device = item["assigned_object"]["device"]["name"]
    interface = item["assigned_object"]["name"]
    address = item["address"]
    device_info[device]['interface_info'][interface].append(address)

print("Here is the completed data structure:")
pprint(device_info)
print()
end_time = time()

run_time = end_time - start_time

print("Run time = {}".format(run_time))


Here are the results of the RESTful script:

blogs/graphql_vs_restful % python3 -i restful_api_query_ams_device_ints.py
device_info is:
{'ams-edge-01': {'interface_info': {}, 'role': 'edge'},
 'ams-edge-02': {'interface_info': {}, 'role': 'edge'},
 'ams-leaf-01': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-02': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-03': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-04': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-05': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-06': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-07': {'interface_info': {}, 'role': 'leaf'},
 'ams-leaf-08': {'interface_info': {}, 'role': 'leaf'}}


The GraphQL query returned all interfaces for a device, regardless of whether 
an ip address was configured; we will match that here.

Gathering interface info for `ams` site.
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=50&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=100&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=150&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=200&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=250&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=300&site=ams
ams_interface_url is None

Adding interface names to device_info for the appropriate device.

Finally, gather the IP address info for each interface.
This RESTful call returns only interfaces that have IP addresses configured.

ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-edge-01
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-edge-02
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-01
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-02
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-03
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-04
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-05
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-06
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-07
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-08

Add the IP address info to device_info.

Here is the completed data structure:
{'ams-edge-01': {'interface_info': {'Ethernet1/1': ['10.11.192.0/32'],
                                    'Ethernet10/1': ['10.11.192.32/32'],
                                    'Ethernet11/1': [],
                                    'Ethernet12/1': [],
                                     < --- snip for brevity --- >
                                    'Ethernet9/1': ['10.11.192.28/32'],
                                    'Loopback0': ['10.11.128.1/32'],
                                    'Management1': []},
                 'role': 'edge'},

 'ams-edge-02': {'interface_info': {'Ethernet1/1': ['10.11.192.1/32'],
                                    'Ethernet10/1': ['10.11.192.34/32'],
                                    < --- snip for brevity --- >
                                    'Loopback0': ['10.11.128.2/32'],
                                    'Management1': []},
                 'role': 'edge'},
 'ams-leaf-01': {'interface_info': {'Ethernet1': ['10.11.192.5/32'],
                                    < --- snip for brevity --- >
                                    'vlan99': ['10.11.64.0/32']},
                 'role': 'leaf'},
 < --- some devices snipped for brevity --- >
 'ams-leaf-07': {'interface_info': {'Ethernet1': ['10.11.192.29/32'],
                                    'Ethernet10': [],
                                    < --- snip for brevity --- >
                                    'vlan99': ['10.11.70.0/32']},
                 'role': 'leaf'},
 'ams-leaf-08': {'interface_info': {'Ethernet1': ['10.11.192.33/32'],
                                    < --- snip for brevity --- >
                                    'vlan99': ['10.11.71.0/32']},
                 'role': 'leaf'}}

Run time = 13.60936713218689
>>> 

Running the script six times produced an average run time of 14.9 seconds.

This script created a data structure that is not identical to the structure created by GraphQL, but is similar in nature and is still parsable.

Final Results

MethodAverage Run Time# of QueriesTime to Create Script
GraphQL2.2 seconds1~ 20 minutes
RESTful14.9 seconds17~ 200 minutes+

NOTE: These results are based on the baseline data in the Nautobot demo sandbox. If someone has modified the database, your actual results may vary a bit.

By any measure, GraphQL is the clear choice here! GraphQL allows a much simpler script that is much more efficient than REST.

Imagine your automation task being able to run an average of 12.2 seconds faster (14.9 – 2.2 seconds) by using GraphQL.

I also don’t want to undersell the amount of time and headache required to create the RESTful script, including parsing the REST data and crafting the data structure: it was not pleasant, and we should not talk about it again. Ever.

GraphQL Considerations for Server Load

Querying with GraphQL results in much less coding and post-processing for the user and is generally much more efficient than RESTful calls that achieve the same result.

However, the load on the Nautobot server must still be considered. Depending on the data you are after and your use case, it may make sense to:

  • Use multiple, targeted GraphQL queries instead of a single GraphQL query with a large scope
  • Use RESTful queries and offload the processing from the Nautobot server, doing the post-processing on your local host

Depending on how many sites and devices you have, the example query below may put undue load on the Nautobot server:

query {
  devices {
    name
    device_role {
      name
    }
    interfaces {
      name
	ip_addresses {
          address
      }
    }
  }
}

This script could cause a lot of undue load on the server because it is not targeted to a site or group of sites.

To ease the load, you could instead do the following:

1. Make a GraphQL query to return all the site names

 query {
     sites {
       name
   }
 }

2. Make additional GraphQL queries to programmatically iterate over each site name within the query parameter devices(site:"<site_name>"):

     query {
       devices(site:"ams") {
         name
         device_role {
           name
         }
         interfaces {
           name
         ip_addresses {
               address
           }
         }
       }
     }
     query {
       devices(site:"bkk") {
         name
         device_role {
           name
         }
         interfaces {
           name
         ip_addresses {
               address
           }
         }
       }
     }

    et cetera . . .

    Wrapping Up

    This case study validates the clear advantages GraphQL offers: simpler and faster automation, higher query efficiency, less time swimming in extraneous data, and thus less time coding. The great part is that Nautobot delivers GraphQL capabilities that you can leverage right now. Please do give it a try.

    If you have questions, you can check out these Network to Code resources for more info:


    Conclusion

    You can also hit us up on the #nautobot channel on NTC Slack.

    Thank you, and have a great day!

    -Tim



    ntc img
    ntc img

    Contact Us to Learn More

    Share details about yourself & someone from our team will reach out to you ASAP!

    Author