I wrote a TypeScript Starter Kit — Here is what I learned

Introducing BrainStrike Starter kit for Node.js + Typescript + React.js + Apollo GraphQL + TypeORM

Sean Dearnaley
IndigoAg.Digital
11 min readJan 20, 2021

--

Colorful logo for brainstrike logo, brain with lightning bolt
BrainStrike TypeScript GraphQL Starter Kit

Introduction

It’s always good to have a working recipe for developing modern applications, and it’s important to keep your tools up to date… stack decisions are incredibly important. I’m a huge fan of GraphQL and TypeScript, so I wanted to develop a starter kit based on those technologies and explore some advanced implementations.

Working as an engineer at Indigo AG involves learning a lot of new technology, in my role as a software engineer I work primarily in the TypeScript + GraphQL ecosystem. I have written about why you should be using TypeScript + GraphQL. Today, I’m writing about my experience developing a starter kit. I call it BrainStrike.

There are different ways to approach this kind of stack. At Indigo AG, we use a microservice architecture and make decisions based on our business requirements. This starter kit has some good examples of advanced implementations including DataLoaders, Relay Style Pagination, Closure Tables, GraphQL Connections and Node Interfaces (with Global Object Identification).

You can use this starter kit to bootstrap your own web applications. There is a server component using Node.js and a client built using React. I’ve included testing implementations and configurations to load your services using Docker and there are some CI utilizing GitHub Actions.

I chose TypeORM for database connections because it’s feature-rich and has good support for TypeScript. I configured linting with ESLint, code formatting with Prettier, and testing implementations with Jest and React Testing Library.

I had several goals for this project:

  1. Develop a full-stack example using all modern components. Server API + Client.
  2. Have it fully typed with absolutely minimum use of escape hatches like the any type.
  3. Working, practical end-to-end type safety. GraphQL + TypeScript are both typed languages, both go well with code generation.
  4. A great developer experience. TypeORM modeling and GraphQL API’s offer a very good DX. I intend to learn more from developing the kit. (I will evaluate the quality of the experience when it’s done, but I’ve already learned a lot about what works and what doesn’t).
  5. Working examples of unit, integration, and end to end testing (written in TypeScript) for both client and server.
  6. Explore advanced implementations of GraphQL / learn / gain experience.

Features

Folder Structure

BrainStrike is structured like a mono-repo with separate folders for clients and server, each with their own package and config. You could set these up in their own repos, switch to each folder to start the respective packages.


brainstrike-typescript-starter
└───.github
│ └───workflows
│ │ nodejs-client-test.yml
│ │ nodejs-server-test.yml
└───client
│ └───node_modules
│ └───public
│ └───src
└───server
│ └───pg-init-scripts
│ └───src
apollo.config.js
codegen.yml
docker-compose.yml
jest.config.js
package.json
tsconfig.json

Application

BrainStrike is a proof of concept for a simple CRUD application, it has a relatively simple server that serves up 2 main entities, Categories and Cards. The idea is that Categories can have many Cards, and those are displayed on a simple React user interface using Material-UI and Apollo GraphQL.

If you haven’t heard of CRUD, it’s an acronym for “Create, Read, Update, Delete.” It is a very common development pattern that describes four different types of functionality. Most data models need to be able to perform these actions. You can use the examples here to develop many typical application patterns.

Prerequisites

Tested with Node 12+, Postgres 11+ required for database. Docker-compose provided for Postgres, but it should be easy to adapt examples to other databases.

TypeScript Configuration

The presence of a tsconfig.json file in a directory indicates that the directory is the root of a TypeScript project. The tsconfig.json file specifies the root files and the compiler options required to compile the project.

See the TypeScript Handbook for a detailed explanation.

Advanced Implementations

One major goal for this project was to explore advanced implementations of GraphQL. I wanted to gain experience with several concepts, and these are slightly beyond the scope of a beginner starter kit.

I went through a few stages of GraphQL API development. I began rather naively by modeling my existing REST API’s, which were already based on databases tables (not a good way to model objects or relationships)… it quickly became apparent that I was much better off representing my data more abstractly, using the powers of types and avoiding traditional polymorphism.

Coming from REST, you may be tempted to re-create over-fetching scenarios. In GraphQL, a particular field must always return the same type. GraphQL fields can return a union of several object types. You can use unions and interfaces instead of polymorphism.

It’s smart to think of your data as a graph, and when you start to unfold that concept, several things become clear. You need strategies for connections between nodes, and you need methods to traverse those nodes. It is more complicated to develop a graph orientated API.

Connections, Graphs, Pagination & the Relay Spec

Connections came from Facebook’s “Relay Cursor Connections Specification.” It’s a powerful concept that is being used by some big players in the software industry. The Relay spec has been called ‘the gold standard’ of pagination, but it’s just a fancier version of cursor-based pagination. It’s more about modeling your data and thinking about data relationships as a graph.

If you’re not already familiar with pagination I recommend reading

’s article. It’s important to consider pagination relatively early because traditional offset based pagination is often not suitable for certain applications, and modifications will require special design considerations. Subjectively, applications are becoming more demanding, with more data, more connections, and some need to be more ‘real-time’.

With connections, “Edges” denote connections between “Nodes”, and those edges themselves can be used to store meta-information about the connection between the nodes (e.g. date connected). It’s a good idea to explore connections before designing your graph. In many ways, it’s a natural conclusion to developing GraphQL networks because graphs are a more natural way of describing data flows. They’re tougher to develop, but extremely powerful and more rewarding over time; think short term pain for long term gain.

Relay cursors have a specific format that can be used by any Relay compliant client but can also be used by Apollo Client. “Opaque” cursors are effectively encoded ID’s (often base64 encoded); they include more than just a unique identifier, such as information about what the ID represents.

Being “opaque” means you hide the implementation, which can be useful for preventing abuse and also can be used to encode extra information. For example, you could encode the item’s type and/or result index. Every “edge” in the Relay spec has its own cursor, which is useful in user interfaces as it allows users to paginate from a particular edge, rather than just the start and end of the respective pagination page.

There are some challenges to devising good database pagination with the Relay spec. It’s more complex than a regular “after” cursor because a full Relay cursor pagination can give you first and last records after or before a given cursor. Consider that you may also need to implement sorting.

To learn more about connections, I recommend reading

’s fantastic article on the subject.

Notice how my Card type example implements a Node interface that will be explained in the next section. The CardConnection type has PageInfo and Edges with a Node and cursor for every edge. You can put meta information into the Edge type.

CardConnection type example
CardConnection output

Connections have deeper property nesting and they can be hard to read.

Apollo Client 3’s new Type & Field policy API’s are useful for connections because it’s easier to manipulate the cache… with the new cache.modify feature, you can “surgically” alter your data without having to parse it first. You don’t need to use the @connection directive- you can get similar functionality using a Field Policy object (eg. using keyArgs).

There are scenarios with pagination where you need better cache access. Sometimes you have to clear cached data that isn’t on the page to avoid it being re-read by other functions. Now you can evict data from the in-memory cache.

Here is a more complex example where I’m updating a connection object in place; cache.modify is a much more efficient way to modify to cache. Note: I can’t just remove the card here because I have to recalculate the pagination page info. It may be simpler/wiser to just re-fetch the query given the complexity, but pagination parameters throw a wrench in that plan. Also note that I’m using references here ( __ref) which are far more lightweight:

useRemoveCard.ts with Apollo Client 3.0 beta 37 using cache.modify

(you can see the old version of this code here)

The Type & Field Policy APIs also make it easier to reuse your pagination cache policies. In the last version of Apollo Client, you had to locate your caching logic alongside your queries, now you can devise a single pagination policy and reuse it across fields.

Ideally, your data graph would be a web of connections, but the logic can be relatively complex. You want the code to be manageable with as much re-usability as possible. It’s not uncommon to have similar pagination policies for all your nodes.

Consider this example from the Apollo Client 3 beta documentation:

Type Policy API — Reusable Pagination Cache Policy

If you can separate your logic and only have to create it once, it should be much easier to maintain a complex graph of nodes.

Node Interfaces (with Global Object Identification)

Node interfaces with global IDs are another concept from the Relay specification. When I first heard about it, I scratched my head a little, but once I understood opaque global IDs it became more logical.

It’s an interesting way of approaching your schema objects. Effectively you have schema objects implement a common Node interface, and that interface has an ID field which implements a global ID. A global ID is another encoded ID, and it often incorporates the original ID of the object and its type, so effectively you can decode that ID and figure out what type the object belongs to.

Global IDs are useful if you want to have a common interface for querying nodes. They’re also useful for queries that could potentially traverse nodes and respond with multiple nodes that have different types. Consider that you could have a global ID that can reference an object in the cache.

GetCategoryNode.graphql — Query implements Node interface
Category implements Node (Server)

In Apollo Client 3.0, FragmentMatcher, HeuristicFragmentMatcher, and IntrospectionFragmentMatcher have all been removed. In the old version of Apollo Client, a combination of those would be used to determine which object belongs to which fragment.

You can now use the InMemoryCache’s possibleTypes option instead (read about how to automatically generate possibleTypes using GraphQL Code Generator later in the article). For more information, see the Defining possibleTypes manually section of the docs.

Here’s an example of possibleTypes, IMHO it’s a far cleaner implementation:

const cache = new InMemoryCache({
possibleTypes: {
Character: ["Jedi", "Droid"],
Test: ["PassingTest", "FailingTest", "SkippedTest"],
Snake: ["Viper", "Python"],
},
});

In my code, Category and Card implement the Node interface, so possibleTypes looks like this. It’s simple now, but imagine when your application scales.

export default {
possibleTypes: {
Node: ['Card', 'Category'],
},
},

I don’t necessarily recommend using a Node interface with Global Identification, as it’s more complicated, but can be good if you want your API to be Relay compliant and/or well suited for graph orientated applications.

One special consideration if you’re using TypeScript — code generators will use advanced type definitions for unions and interfaces, notably discriminated unions.

It took me a moment to realize why my code was throwing errors. TypeScript will check that you are covering all variants of the discriminated union properly; often you have to use a switch statement to access the right union properties.

Apollo Hooks

React hooks have been a game-changer. Apollo is designed for modern React and Apollo Client 3.0 has hooks with full TypeScript support. The official full-stack example client is written in TypeScript.

TypeScript + Code Gen

I’ve become a huge fan of using TypeScript with Apollo GraphQL. TypeScript at the language level combined with GraphQL at the API boundaries offers code introspection and safety. Type annotations give you a stronger sense of a programmer’s intentions. They have made me a better developer.

Code generators can generate your types for free. There are a few popular code generation tools… Apollo Codegen is available via the official Apollo CLI.

TypeGraphQL allows you to define your schema programmatically using classes and decorators….

BrainStrike uses GraphQL Code Generator. It has a fantastic selection of plugins, which can be used to generate your server resolver types and they already have some support for Apollo Client 3 including an automatic fragment-matcher plugin. It’s conceivable that future code generation could be used for the new Type & Field policy APIs. It’s not hard to write your own plugins!

Here is an example of my GraphQL Code Generator client config. You can see I’m using it to output my TypeScript types for operations. I’m also using it to generate Apollo hooks for all my .graphql documents. I have a config to format the code with Prettier after it’s written. I’m generating a whole schema introspection file, I’m also using the fragment-matcher plugin configured for Apollo Client 3 that automatically generates possibleTypes.

Note the config section which has parameters to configure the new @apollo/client import locations.

GraphQL Code Generatorcodegen.yml for Apollo Client 3

Here is an example of an index file with possible types imported automatically. These are generated by the GraphQL Code Generator fragment-matcher plugin, this example also shows different type policies:

index.ts with Possible Types from GraphQL Code Generator

In my opinion, the Apollo CLI Codegen has slightly more readable type definitions, it takes a different approach to files, it writes multiple files for your different GQL queries.

GraphQL Code Generator can be configured to wrap your GraphQL documents in hooks, so a query named GetCard could have a hook named useGetCardQuery , you simply write queries into .graphql files and code generator wraps them into a single generated .ts file.

One downside of GraphQL Code Generator’s approach is that the generated type code can be a little overwhelming for new users of TypeScript. It’s more verbose and there are more types to consider. That being said, GraphQL Code Generator is highly configurable.

I highly recommend trying TypeScript with GraphQL. Try out the different code generators, they’re all great projects.

Unit Testing, Integration Testing and E2E Tests

BrainStrike includes complete examples of working tests using modern implementations and a recent version of React Testing Library — I’ve written an article about my experience, which should give a good breakdown of the methodology and shows how BrainStrike utilizes GitHub Actions for cheap CI.

Conclusion

I learned a lot creating BrainStrike and there is more to learn. In the future, I’d like to implement authentication and more advanced user interface features. I’ll be maintaining the project, reading issues and keeping it as up to date as much as possible. Good luck with your own projects! And feel free to contribute to this project by submitting your own pull requests.

--

--

Sean Dearnaley
IndigoAg.Digital

I have worked on different applications for the music industry, government, education and agriculture.