Full-stack TypeScript and GraphQL
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:
- End-to-end type safety
- Auto-generated clients (for both frontend and inter-service calls) via Graphql Code Generator
- Separation of concerns for our microservices via GraphQL Federation
- Schema checks, tracing, and health metrics via Apollo Studio
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-codegen
package 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 thegraphql-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!