Designing a Concise and Consistent GraphQL API

The web world moves quickly; every developer involved with this ecosystem is painfully aware of this fact. The state-of-the-art stack you start building your startup with today is considered ancient technology within the next 3 months. Even so, there are a few timeless (at least at Javascript-speed) gems produced in this world. GraphQL is a technology that has been around for about 2 years now and it integrates well with the React/Relay stack to provide a more complete ecosystem for a web developer to work in. While Facebook and others have done a fine job discussing the merits of GraphQL and why you would use it (read: I'll let them convince you it's a good idea), we're going to discuss how to harness its power in a very reproducible way.

Difficulties with GraphQL

GraphQL is a great piece of technology. However, as a developer you're left with few guidelines to get moving quickly. In and of itself, GraphQL is a very non-opinionated interchange format. More specifically, it describes a graph-based structure for accessing and modifying data. While data access may seem like a straightforward task when previewing GraphQL, mutations are vague and very under-spec'ed out of box. While in theory (and practice once you've wrapped your head around it) this is actually a good thing for maximum flexibility, it leaves many developers scratching their heads wondering what to do.

GraphQL gains much of its power from this broad flexibility. At the same time, this flexibility creates a lot of cognitive overhead on developers. To date, most-- if not all-- GraphQL API's I have seen are hand-written. This is a cumbersome task. As we move to client-side apps, the backend servers are becoming more like secure datastores which enforce business constraints on data. This was likely always the case in traditional web development as well, but the backend was often merged with UI and UI generation code; as a result, this distinction was blurred. The only other logic performed on these servers is now often private computations which should be concealed from the clients' code. Moreover, with hand-written endpoints, it's difficult to guarantee both API consistency and complete functionality from one endpoint to the next.

Ultimately, this creates an API ecosystem where each endpoint is different. Namely, understanding how to operate one endpoint within my own GraphQL application doesn't necessarily guarantee I can properly operate others (much less other GraphQL apps in general). This would then require copious amounts of documentation on each endpoint for any developer to get up and running effectively. This is more reminiscent of my hardware days-- scanning through datasheets of similar components-- than it is of developing highly generic and reusable software components.

Solving these GraphQL Difficulties

As luck would have it, we already have a solution to eliminating the boilerplate for hand-written endpoints. In 2015, we built Elide. Elide enables developers to model their data using JPA, communicate with arbitrary datastores, secure it using meaningful expressions, and write custom code where necessary. It's been a large-scale, multi-year effort, but it has proven very effective on our own products. In any case, this is a solution designed to solve all the problems that fallout from hand-written endpoints: API consistency, uniform functionality, proper security, developer boredom, etc. The only problem: it didn't support GraphQL (and is only now in the 4.0 beta).

Initially, when we went out to build Elide, rather than reinventing the wheel we sought a standardized web API solution. GraphQL hadn't yet gained any traction (and iirc, it wasn't even fully released). As a result, we found JSON API and decided to build Elide around supporting this technology. It was opinionated and generating the API was straightforward. While JSON API has a lot of strengths, we also found some issues with the opinionated stances in which it took (more on that in a later post). Now you may be thinking to yourself, "Wait a minute, why is this important? I thought this post was on GraphQL." Well, it turns out that working so intimately with JSON API, we used-- what we believe to be-- some of its best ideas to inform ourselves on how to generate uniform, consistent, and automatable GraphQL API's.

Now that we have discovered a solution for minimizing endpoint code, there was a new looming question. How do we generate a consistent GraphQL API? That is, for any endpoint, all you need to know is the data model. From then on, you will have a standard set of tools available to you. Of course you could extend any particular model with special logic as needed, but generally speaking, there would exist a fully supported, common toolset for all models.

Designing a GraphQL API

Our motivation for building out GraphQL support in Elide was to more easily adopt its client-side ecosystem. It will not replace JSON API, but instead live alongside it for users to choose which solution is best for their project. However, this imposes an additional set of constraints; subtle implementation details such as pagination must be compatible with the existing tooling (i.e. Apollo or Relay). But one problem at a time: while GraphQL query-schemes appear to be well worked out, we first need to solve the problem for having a consistent means of object mutation.

Consistent GraphQL API Mutation

Our first goal was to find a consistent way of specifying GraphQL mutations; basically, any time you wish to insert or update data. After several ideas, we came back to an approach inspired by JSON API and REST (though it is certainly not REST). In summary, we turn each JPA relationship and Elide rootable types into objects which take GraphQL arguments. The arguments are op, ids, and data. Without going into all the details (the current spec can show examples), this allows us to support the same operations in a decidable way across all exposed entities in our GraphQL API. A brief description of each parameter is below:

  • op. This parameter describes the operation to be performed. When unspecified, this defaults to a FETCH operation, but it can also take on values such as UPSERT, REMOVE, REPLACE, and DELETE.
  • ids. When provided, this list of id's is used to filter the collection on which the operation is being performed.
  • data. The data argument is used for UPSERT'ing and REPLACE'ing. It specifies the new input data from the user.

With these three arguments, a user can perform arbitrary data operations on their models.

An Apollo/Relay-Ready GraphQL API

Automating an API that is Apollo- and Relay-compatible adds a few more layers of complexity. In short, a model in our originally proposed scheme would look like:

The example above will fetch a book object from the system with id and id of 1. It will return its title and all of the names of its associated authors. Pretty straightforward, right?

Well, when accounting for important concepts like pagination and so forth, this will not work with Relay out of box. As a result, we adopted Relay's Cursor Connections for maximum compatibility. While the scheme is still what we have proposed, there are now 2 additional layers of indirection for each model containing metadata (namely, edges and node objects). These layers are used for additional metadata. See below:

As you can see, the layers of indirection make the format a little bit uglier, but the same overall concepts apply.

Conclusion

GraphQL has many great ideas and an incredible amount of flexibility. However, it's difficult to avoid writing many hand-written endpoints and maintaining consistency across all of them. However, Elide was invented to solve one of these problems and has recently implemented a consistent method for generating GraphQL API's. If you're looking for a quick, out-of-box solution, I recommend considering Elide for your next project (to get started, see the example standalone project). In any case, when you build your next GraphQL API, be sure to think through these problems. If you don't adopt our scheme directly, at least be aware of the problems it solves. Good luck out there!

Programming Styles: Procedural, Object Oriented, and Functional

While there exist many programming paradigms, there are three popular styles frequently used today. Namely, procedural programming, object oriented programming, and functional programming. While many languages adopt features across paradigms, most languages idiomatically prefer one style over the other. That is to say, even though these three methodologies are not mutually exclusive, the practical application within a language may have varying levels of support based on language features and community sentiment.

All of these styles have been known for many years and could easily be a post (or book) on their own. However, I will attempt to briefly introduce these styles in a way that I propose is similar to "past, present, and future." While I strongly believe all three paradigms will continue to exist long into the foreseeable future, I would be remissed to suggest that the industry isn't continuously evolving. As a result, while I believe any strong programmer should-- at the very least-- be adequately familiar with all of these styles, I do believe emphasis should be placed on learning more relevant technologies.

Procedural Programming

Common Languages: C, Fortran, BASIC, COBOL, Go

As you can see by the common language list, procedural programming has all but fallen out of favor for modern language design. With the exception of Go, all the languages on that list are at least 50 years old. While most newer languages are not adopting this programming style, many systems still run on these languages. As a result, they have certainly stood the test of time which would imply there are good ideas here.

So what is this procedural programming thing? In a single sentence: a collection of predefined sequential statements (i.e. procedures) which manipulate system state. In a less dense way, procedural programming allows programmers to write re-usable blocks of code to perform actions within their program. If you've done a lot of programming in the 21st century, this concept may seem painfully obvious to you since almost every modern language has first-class support for methods, libraries, and even package management nowadays.

A key distinction here from other paradigms (i.e. object oriented), however, is that your procedures and data are entirely distinct. The procedure takes some input, mutates it in some way, and optionally returns some status code to the caller.

Procedural Example

Let's examine a bit of C code to see procedural programming in action.

For you C programmers out there, this probably looks pretty familiar: it's a naive, modified memcpy implementation. Namely, if the provided num_bytes value is less than 0 then the algorithm immediately returns with an error code otherwise it proceeds.

This example is demonstrative of typical procedural form. If your application often has to copy memory, you wouldn't want to have to write this procedure several times. You can see that we named our procedure "mycpy" for it to be reused throughout our application. Moreover, our return value is a status code rather than a usable value. Namely, it indicates whether or not our procedure succeeded or failed. Finally, the actual result of our computation is stored in the output argument provided by the caller.

While not all functions will be constructed this way in procedural languages (i.e. it is not uncommon for these languages to return the result directly if it is a primitive type), the general form for complex computations is as follows:

  1. Take the "result" object as input from the caller
  2. Perform predefined computation
  3. Store result in the "result" object (i.e. state mutation)
  4. Return appropriate status code (i.e. if error encountered provide error code, else success code)

In summary, procedural languages exhibit great ideas around code reuse and how to manage mutations and errors cleanly. Code written in procedural style often reads fairly well and is easy to follow for single-threaded applications. However, with all of the state mutation, it can become complicated for multi-threaded applications which are omnipresent in today's technology.

Object Oriented Programming

Common Languages: Java, C++, Python, Objective-C, C#

Another well-known programming paradigm is Object Oriented Programming (OOP). In my estimation-- based on both the job listings I've seen and the solicitations I've received-- this is the most popular style in-use today. If you don't explicitly see familiarity with OOP on the job requirements, you will almost certainly see at least one of these languages listed here on many job requisitions for software engineers today. These languages are typically the "heavy hitters." That is, they're know to be the languages people usually go to for performance, reliability, and scale: especially in well-established and/or enterprise companies.

Object oriented programming is-- as you may have guessed-- centered around the concept of objects. Much like procedural programming, we'll continue to mutate state. However, rather than having the caller join together the data and the procedure, we'll instead combine the two. This is the foundation of what an object is; it's both data and a set of common operations that can perform computation on that data.

At first glance, this sounds like a marginal improvement over procedural programming. The most obvious benefit is that since your methods are now bound to your data, you don't have to pass the data object in. That's great, but is it really worth all this fuss? Well, as it turns out, having objects enables an entirely new class of abstractions to work with. You now have inheritance, encapsulation, and polymorphism (IEP). Those are a couple of big words:

  • InheritanceThis is when an object derives a set of properties (methods and data) from another object. It typically represents an "is a" relationship (i.e. a Car is a Vehicle).
  • Encapsulation. You can now hide all the internal details about your model. In theory, if you have modeled your objects properly, you can avoid leaking any internal details and the caller can use interfaces without having to look at the code.
  • Polymorphism. This allows you to treat a specific type as a more generic type; it's the other side of inheritance. Specifically, if you want to perform an operation on all Vehicle classes in your system, you can do so. Whether you provide it a Car or a Boat-- as long as they both inherit Vehicle-- is irrelevant. You can simply treat them as vehicles without any additional code.

It should now be clearer how OOP can actually be a stark improvement-- by way of code reuse and, ideally, more powerful abstractions-- over procedural programming. While we'll be focusing solely on what we have mentioned for now, OOP enables other design patterns as well (mixins, object composition, etc.) that we will discuss in a separate post. Even though we don't go through them here, curious readers should investigate further to see how these patterns behave and what problems they solve.

Object Oriented Example

Below is an example in Java:

This example continues from where I left off above. That is, there is a base class which we refer to as Vehicle which does not have any implementation itself. However, we have two types of vehicle which inherit the Vehicle class: Car and Boat. Each of these inheriting classes actually implement the getName() function. Finally, if you observe our use of it in our main function, you'll realize that both our Car and Boat are stored in a list of Vehicle objects (i.e. polymorphism). Likewise, we also benefit from code reuse in this abstraction. Observe that we only had to implement a single turnLeft() and getDirection() function, and both vehicles were able to gain this functionality without additional code. The direction data variable is silently stored within the object (i.e. encapsulation). As you can see, putting the data and methods in the same container has provided us with some additional code reuse power.

Overall, object oriented programming is incredibly important in the industry today. While it lends itself well to natural abstractions such as the Vehicle example, it often requires a lot of forethought and/or refactoring to avoid leaky abstractions in complex systems. As a result, design can often become more difficult in object-oriented systems than procedural systems due to its flexibility. However, similar problems still exist in multi-threaded applications as they do in procedural systems: when the codebase becomes large, it is often difficult to follow the mutations through the system. This problem is somewhat mitigated as entire objects can be written to be made thread-safe, but any paradigm which advocates data mutation opens itself up to the same class of problems. While nothing is perfect, OOP seems to strike a balance for most programmers. Namely, it's an understandable concept with great power and flexibility to eliminate code redundancy.

Functional Programming

Common Languages: Haskell, Lisp, OCaml, ML, Scheme

In my opinion, this section begins the future of programming. While functional programming has been around for quite some time, its design eliminates entire classes of problems encountered in other languages. Similarly, many traditional arguments against the practicality of functional programming (i.e. performance) are now negligible for all except the most specialized use-cases. Then again, I likely wouldn't put the JVM on an embedded system either, so this problem is not purely related to the functional paradigm.

Functional programming (FP) takes a step away from what we've been discussing. Rather than thinking about how the computer executes instructions and moves data, we instead look at our problem in a more logical way. There are no longer methods or procedures but instead functions. That is, a set of operations that take input and produce output. Likewise, data and functions are distinct elements; we no longer couple the two like in OOP. One of the most important notions in FP is that of immutable data. Logically, once something is created it cannot be modified. If you want to change an object's values, create a new instance with the updated data and return that to the caller. Before you stop reading here, remember two points I've been making:

  1. Even if there were full copies of your data each time you needed to make a change, processors are fast enough today for most applications
  2. I've mentioned that this is a logical model. More specifically, compilers can optionally optimize in clever ways to mutate data if it is appropriate
    1. The benefit here is that the programmer doesn't have to worry about this and, therefore, mistakes are minimized

Functional programming lends itself well to a lot of other cool concepts (i.e. lazily initialized collections, lazy function evaluation, correctness proofs, etc.), but we don't have the space to go into all of that right here. However, you'll notice almost all modern languages are adopting the functional programming paradigm. While I tout FP as being the "future," the fact is that it's already here. Python has always supported functional map, reduce, lambdas, and list comprehensions. Similarly, C++ and Java have adopted a whole set of functional concepts in their recent releases as first-class citizens in the language. Moreover, with callbacks and the like, Javascript makes use of a lot of functional concepts and it is often idiomatic to write functional Javascript. What I'm getting at is that the industry is clearly learning. There has been a lot of griping that "functional is hard," but as people begin to understand it they are realizing that it is actually a cleaner, more concise way to model the world. Since most people are not writing super specialized embedded systems, functional programming languages often exceed their minimum technological requirements.

Functional Example

As you can tell, I'm a bit biased. I really like functional programming (even though I write mostly OOP at work). In any case, I will provide an example in Haskell to demonstrate some of FP's power. In FP we're going to model things as higher level abstractions. With this in mind, this is neither the cleanest nor most concise way to express this in Haskell, but it should be explicit about what's going on:

In this example, you can notice several things which we have already discussed. First of all, we create a Vehicle class which describes the behavior of all vehicle types in our program. The next bit of code then defines our data objects (i.e. records). You will notice that there is no code associated with these data objects.

Next, we get to a bit of implementation code. Namely, we define that both data types are indeed Vehicle types conforming to the class definition we outlined above. From this point on, we can generically treat each data type as a Vehicle. While this may not seem incredibly useful in this particular example, it enables us to access any code which knows how to operate on the Vehicle class. In this example, those are leftDirection and printVehicleInfo. However, this feature becomes particularly useful for very common operations such as Traversable operations.

Finally, if you direct your attention to the main method, notice the order of our calls. We run a few turnLeft calls on our boat and car before printing their information. Then, after that, we print the original information. It is important to recognize that the original boat and car instances remain unchanged even though the results of turnLeft calls were correct.

While not completely evident here, the power in functional programming is in its ability to generalize concepts. Similarly, since data is separate from functions which operate on it, it also increases code reuse. If you have a data type provided by someone else and you want to perform a certain set of well-defined operations on it, you can overload the appropriate typeclass to do this without having to reimplement any additional functionality. Moreover, the immutability of data structures dramatically improves the logical model of your system. Namely, in multi-threaded applications, nothing can "accidentally" change anything else. This allows us to mostly do away with locks and other pitfalls associated with multi-threaded programming.

Conclusion

There are many programming paradigms and a lot to know about each before determining which is best for your use case. While I cannot assert that there is single best solution for all problems, I do claim that there is often a better choice for a specific problem. I have introduced you to three major styles in programming today that all have practical relevance. It is well worth your time to take a deeper look into any of these paradigms and determine which may be best fit for your next project.