End-to-end Type Safety in Clean Architecture
Tags: typescript, architecture, graphql, react
Published: 2020-05-22 10:23:56

---
title: End-to-end Type Safety in Clean Architecture
published: true
description: "Let's create a completely type-safe web application in a Clean Architecture using a couple of modern libraries. Such systems are orders of magnitude more reliable than the untyped counterparts. Plus, they are easier to understand, maintain and refactor. Tech: TypeScript, GraphQL, MongoDB, React."
author: Charles Szilagyi
tags: #typescript #architecture #graphql #react
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/j6wee1yzjnqxmun90r69.jpg
canonical_url: https://charlesagile.com/end-to-end-type-safety
---

Let's create a completely type-safe web application in a Clean Architecture using a couple of modern libraries. Such systems are orders of magnitude more reliable than the untyped counterparts. Plus, they are easier to understand, maintain and refactor. Tech: TypeScript, GraphQL, MongoDB, React.

💡 The sample code is on Github: https://github.com/thekarel/best-practices-example

# End to End Type Safety

In this post I show you how to build a full-stack web application that is type-safe across all layers of the architecture: from the domain models to the services, repositories, the HTTP transport layer (GraphQL) and client-side UI. It's more than just typing up every interface in the codebase: in this example, all significant interfaces and types are derived from higher-order ones - usually from the [domain modules](https://thekarel.gitbook.io/best-practices/the-big-picture/architecture/domain).

> There is only a single source of truth for types

This means that changes to high-level interfaces cascade through the whole stack. The type checker will be able to spot a mismatch in any one of the layers.

![Github code repository](https://dev-to-uploads.s3.amazonaws.com/i/g6gsqbuxtq3w81u5kl6c.png "Github code repository")

# Benefits

The practical benefits are pretty significant:

* The linter warns you of potential bugs before you even run a test or let alone build the app
* You need to write far fewer [unit tests](https://thekarel.gitbook.io/best-practices/testing/purpose/unit) than otherwise because the whole codebase relies on interconnected type definitions.
* The codebase is easier to understand as the same interfaces are repeated (maybe with slight modifications)
* Since everything is typed the code is [self-documenting](https://thekarel.gitbook.io/best-practices/source-code/comments)
* When you change the code - fix, refactor or improve - you get *instant feedback* about the health of your codebase in the IDE or by running `tsc`.

Experience shows that even large refactoring can be successfully done on such codebase, solely based on static type checking. Of course, it is not a substitute for [End-to-end tests](https://thekarel.gitbook.io/best-practices/testing/purpose/e2e).

All in all, I think such stack eliminates some significant sources of bugs that would otherwise exist because the codebase complexity exceeds a limit. We're incapable of remembering every data shape, type and interface. Apart from fewer bugs, you'd also benefit from higher confidence and faster development throughput. Win-win?

# Clean Architecture TL;DR

The [architecture](https://thekarel.gitbook.io/best-practices/the-big-picture/architecture) of this example follows [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) principles.

This, in a nutshell, means that:

* The app is sliced into layers, starting from the deepest: domain (entities), services (use cases), transport (GraphQL in this case), repository (abstraction over MongoDB), UI (React, closest to the user)
* There is a *strictly unidirectional dependency arrow*: layers that are deeper in the stack can never refer to any code in outer layers

The second rule implies that the domain module will never import or refer to anything defined in other layers. The services receive "tools" to get and save data (etc.) via dependency injection. The repository can know about domain entities (but not much else). The transport layer is a smart cookie and knows about the domain, services and repositories (this is the dependency injection root). The UI, ideally, is limited to the GraphQL types, and maybe the domain entities.

![The original Clean Architecture diagram](https://dev-to-uploads.s3.amazonaws.com/i/7dztbl7gdm1xdfhbge6f.jpg "The original Clean Architecture diagram")

*The original Clean Architecture diagram. Image from https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html*

Head over to https://thekarel.gitbook.io/best-practices/the-big-picture/architecture for a detailed treatment.

# Tech Stack

Reminder: there is a complete example available at https://github.com/thekarel/best-practices-example

The technologies and libraries I use are the following:

* [TypeScript](https://www.typescriptlang.org/)
* [Apollo Server](https://github.com/apollographql/apollo-server)
* [TypeGraphQL](https://github.com/MichalLytek/type-graphql)
* [GraphQL Codegen](https://github.com/dotansimha/graphql-code-generator)
* [React](https://reactjs.org/)
* [GraphQL Hooks](https://github.com/nearform/graphql-hooks)
* [MongoDB](https://mongodb.github.io/node-mongodb-native/)

I assume you are relatively familiar with most of these tools already. I'll focus on two libraries that are probably not so widely used and also highlight a couple of essential solutions.

Let's look at each layer one by one and see how this stack hangs together.

# Layers

## Domain

Technically this is the simplest slice of the stack. The entities are pure TypeScript interfaces. For example, an `Order` looks like this:

```typescript
import {Cupcake} from '../cupcake/Cupcake'

export interface Order {
  id: string
  customerName: string
  deliveryAddress: string
  items: Cupcake[]
}
```

The corresponding `Cupcake` is

```typescript
import {Feature} from './Feature'
import {Kind} from './Kind'

export interface Cupcake {
  kind: Kind
  features: Feature[]
  name: string
}
```

The critical fact is that all subsequent layers will refer back to these definitions in some shape or form.

## Services

The [Service layer](https://thekarel.gitbook.io/best-practices/the-big-picture/architecture/services), also known as Use Cases defines the possible operations on our entities. In this example, these include creating and reading `Order`s.

The [domain entities](#Domain) are very abstract, but you might be thinking: creating an order is a concrete operation and must be able to talk to a database etc. This fact seems to contradict the dependency arrow rule mentioned above.

>  Dependency Arrow Rule: Layers that are deeper in the stack can never refer to any code in outer layers

The solution is to define dependency interfaces in the Service layer. For example, the `OrderService` defines an `OrderRepository` interface. This way the service itself will not have to know anything about the way orders are stored, but can dictate the shape of data going in and coming out of a repository - the rest is an implementation detail, from this point of view:

```typescript
import {Order} from '@cupcake/domain'

export interface OrderRepository {
  connect(): Promise<void>
  save(order: Order): Promise<void>
  load(id: string): Promise<Order | undefined>
  all(): Promise<Order[]>
}
```

In terms of end to end type safety, please note how the `save` method takes a *domain Order* and similarly how the `load` method returns one. This ensures that we can use different storage methods without breaking the contract (see below).

The domain interfaces reappear in similar ways across the whole stack.

## Repository

As hinted above, the [repository is a data persistence abstraction](https://thekarel.gitbook.io/best-practices/the-big-picture/architecture/persistence). Since it implements a higher-level interface definition, we can use different storage strategies in our app depending on the circumstances. Compare the following two repository implementations: one saves into memory, the other into a real database:

### OrderRepositoryMemory

```typescript
import {OrderRepository} from '@cupcake/services'
import {Order} from '@cupcake/domain'

export class OrderRepositoryMemory implements OrderRepository {
  private orders: Map<string, Order> = new Map()

  async connect() {
    return
  }

  async save(order: Order) {
    this.orders.set(order.id, order)
  }

  async load(id: string) {
    return this.orders.get(id)
  }

  async all() {
    return Array.from(this.orders.values())
  }
}
```

### OrderRepositoryMongo

```typescript
import {Order} from '@cupcake/domain'
import {OrderRepository} from '@cupcake/services'
import {Collection, MongoClient} from 'mongodb'

export class OrderRepositoryMongo implements OrderRepository {
  client: MongoClient
  dbName = 'cupcakes'
  collectionName = 'cupcakes'
  collection?: Collection<Order>

  constructor(private readonly url: string) {
    this.client = new MongoClient(this.url, {useUnifiedTopology: true})
  }

  async connect() {
    await this.client.connect()

    this.collection = this.client.db(this.dbName).collection<Order>(this.collectionName)
  }

  async save(order: Order) {
    if (!this.collection) {
      throw new Error('Connect first')
    }

    await this.collection.insert(order)
  }
  
  // etc
}
```

> Look at the example codebase to [see how different repositories are injected](https://github.com/thekarel/best-practices-example/blob/master/modules/graph/src/getProductionContainer.ts#L13)

Another equally important fact to note is that all type definitions are picked up from the domain and services layers.

Probably the most significant feature in the type safety context is the fact that we enforce the database documents' shape to match the domain entities:

```typescript
this.collection = this.client.db(this.dbName).collection<Order>
```

This is to ensure the primary rule of persistence in Clean Architecture:

> A repository takes a domain entity and returns a domain entity or a list of entities

The type-safety of the database layer itself is an important fact: it guarantees that the data entering our system (from the outside world) will match the expected domain shape. In other words, we ensure that everything inside the application boundaries is of known shape.

## GraphQL

The example codebase uses GraphQL as the [transport layer solution](https://thekarel.gitbook.io/best-practices/the-big-picture/architecture/http).

GraphQL types are sometimes defined using the ["GraphQL schema language"](https://graphql.org/learn/schema/#type-language), for example:

```gql
type Customer {
  name: String!
  address: String!
}
```

Using the schema language has one serious disadvantage: it's not possible to refer to domain types using GraphQL's schema. It's time to look at...

### TypeGraphQL

![TypeGraphQL](https://dev-to-uploads.s3.amazonaws.com/i/f8m3fhh9w8cbj2ha0mtc.png "TypeGraphQL")

[TypeGraphQL](https://typegraphql.com/) allows us to define GraphQL schemas using TypeScript classes. Using `implements` we can then refer back to domain interfaces. For example, this is how a `Cupcake` interface looks like in the [example Graph](https://github.com/thekarel/best-practices-example/tree/master/modules/graph/src):

```typescript
import {Cupcake as DomainCupcake, Order as DomainOrder} from '@cupcake/domain'
import {Field, ID, ObjectType} from 'type-graphql'
import {Cupcake} from '../cupcake/Cupcake'

@ObjectType()
export class Order implements DomainOrder {
  @Field(() => ID)
  id!: string

  @Field()
  customerName!: string

  @Field()
  deliveryAddress!: string

  @Field(() => [Cupcake])
  items!: DomainCupcake[]
}
```

[Generating the final schema](https://github.com/thekarel/best-practices-example/blob/master/modules/graph/src/generateSchema.ts) from these classes is trivial (don't worry about the container, it has nothing to do with type-safety):

```typescript
import {AwilixContainer} from 'awilix'
import {buildSchemaSync} from 'type-graphql'
import {OrderResolver} from './order/OrderResolver'

export const generateSchema = (container: AwilixContainer) =>
  buildSchemaSync({
    resolvers: [OrderResolver],
    container: {
      get: (constructor) => container.build(constructor),
    },
  })
```

The Graph imports the domain type definitions and turns them into strong guarantees: anyone sending a `Cupcake` to the server must conform to the domain schema (or the request is rejected). What we achieve with this is significant the same way as it was for the repository: the data coming into our system from the outside world is guaranteed to match our expectations.

> The data coming into our system from the outside world is guaranteed to match our expectations

## UI

The example app uses a React UI - but any UI library would work.

The crucial question is instead, how do we map from our Graph or domain entities to definitions that are useable in the UI?

Ideally, the UI only knows about the Graph interfaces: these are the "things" that are sent towards the client, and in turn, this is what the client sends back.

> Ideally, the UI only knows about the Graph interfaces

GraphQL being what it is, there are other, more intricate questions concerning queries and mutations - it can get complicated quickly. Manually copying all these interfaces from Grap to the UI codebase, and keeping them updated is hopeless.

Hence, we look at the last piece of the puzzle: generating static TypeScript types from GraphQL schemas.

### GraphQL Codegen

![GraphQL Code Generator](https://dev-to-uploads.s3.amazonaws.com/i/o12krkl1i58l6j9qh0gh.png "GraphQL Code Generator")

> [GraphQL Code Generator](https://graphql-code-generator.com/) is a CLI tool that can generate TypeScript typings out of a GraphQL schema.

The implementation is relatively simple and it only touches the UI project.

First, define a [configuration file](https://github.com/thekarel/best-practices-example/blob/master/modules/ui/codegen.yml) in \`ui/codegen.yml\`:

```yml
schema: http://localhost:8888/
generates:
  src/graphQLTypes.ts:
    hooks:
      afterOneFileWrite:
        - prettier --write
    plugins:
      - typescript
      - typescript-operations
config:
  namingConvention:
    enumValues: keep
```

Add a command to [package.json](https://github.com/thekarel/best-practices-example/blob/master/modules/ui/package.json):

```json
  "scripts": {
    "typegen": "graphql-codegen"
  }
```

When you know that the GraphQL schema has changed - this is [easy in a monorepo](https://thekarel.gitbook.io/best-practices/constraints/monorepo) - run the `typegen` command in the UI to generate a local type definition of the Graph types. You commit these to the codebase just like any hand-written code.

Having access to these types enables UI components to refer to the Graph types when [making a request or creating a payload](https://github.com/thekarel/best-practices-example/blob/master/modules/ui/src/dashboard/DashboardContainer.tsx):

```typescript
import {Feature, Kind, MutationCreateOrderArgs, Query} from '../graphQLTypes'

// later
const [fetchOrders, ordersFetchStatus] = useManualQuery<{orders: Query['orders']}>(ordersQuery)
  React.useEffect(() => {
    fetchOrders()
  }, [])


  const dumbOrderArgs: MutationCreateOrderArgs = {
    deliveryAddress: 'New York',
    customerName: 'Mr. Muffin',
    items: [
      {kind: Kind.savoury, features: [Feature.sugarFree], name: 'One'},
      {kind: Kind.sweet, features: [Feature.vegan], name: 'Two'},
      {kind: Kind.sweet, features: [Feature.exclusive], name: 'Three'},
    ],
  }
```

# The End

As with any code example, this is a slight simplification. Life is always a bit different and undoubtedly more challenging. I haven't touched the topic of evolving interfaces (data shapes), for example. Still, I think these ideas and tools provide a solid foundation to build on.

Relying on clean architecture and a type-safe codebase will make the products we make better and our lives more comfortable at the same time.

> The more complex you build, the more type safety you need!

Have I missed something? Please [let me know](https://twitter.com/thekarel)!