Full-stack TypeScript and GraphQL

John Mastro
Dandy Engineering, Product & Data Blog
3 min readJul 2, 2021

TypeScript, GraphQL, and NestJS form core parts of our stack at Dandy. Together, they provide a powerful ecosystem and toolset that enables us to move quickly while maintaining a robust and reliable system. The key features they provide for us are:

Each part of this stack is individually well-documented, but when starting out it can be difficult to get a full picture of how to combine them. This is the first in a series of articles where we’ll lay out how and why we use these tools, some of the things they enable for us, and some pitfalls to be aware of.

To start, we’ll take a look at the system from three perspectives: the code generation that ties the components together seamlessly, how requests flow at runtime, and what the code looks like from the perspective of a developer working on services and applications within the system.

Code generation

Here’s the code generation pipeline, which operates in a few stages:

Stepping through this:

  • We have multiple backend services and user-facing React applications.
  • Our backend services define graphql object types using NestJS’s GraphQL decorators, plus resolvers to handle queries and mutations on those objects.
  • Backend services generate their own GraphQL schemas using NestJS’s schema file generation feature.
  • Our Apollo Gateway service consumes those schemas and generates a federated schema combining them, allowing clients to send GraphQL operations to any of our services at the same endpoint.
  • Our client-generation packages use the graphql-codegenpackage and federated schema to generate Typescript types for all of the objects in our schema.
  • graphql-codegen is then used to generate types and runtime code for operations defined inside .graphql files in our client packages. Our frontend client package generates React hooks using Apollo Client, while our backend client package uses the graphql-operations npm package to generate a client class.

Request processing

There’s a lot going on there, but things are much simpler when we process requests at run-time:

The key thing here is that apps never need to interact directly with backend services, nor backend services with each other — all apps and services submit queries and mutations to the Gateway, which then dispatches them appropriately. Importantly, this is true even if the request requires data to be composed from multiple services.

Code

To wrap up, let’s both take a step back to code generation while zooming in to take a brief look at what the actual code (both developed and generated) looks like in practice.

As you’ll recall, everything starts with classes and resolvers defined in our backend services:

From those, a we generate a GraphQL schema including:

Next we compose the federated schema (which merges the schemas of our various services), but we’ll omit that here since our example only uses a single object type from a single service.

Then some boilerplate is generated to make the data type and query usable from the frontend:

Which ultimately looks something like:

The backend client and usage is similar, except the generated code leverages the environment-agnostic graphql-request package instead of Apollo/React hooks.

Conclusion

This is a very simple example, but importantly it would be just as simple to use if the data type and resolver were much more complicated, or even if the query required data to be assembled from multiple services.

In future articles, we’ll go into more depth on some of the interesting technologies and techniques that we’ve just scratched the surface of here. If there’s anything you’re especially interested to hear more about, reach out and let us know!

--

--