One of the hardest problems I find when building web applications is wiring up the frontend to the backend. I spend way more time getting the backend set up with the correct data model, data structures and defining the API and then when it comes to wiring up the frontend, trying to remember the schema and correct types that the API expects I always get it wrong.
Thankfully, with the right toolchain it is now possible to end-to-end type safety from your database, through your application code, and into the client by using a select number of tools for the job.
In a nutshell, type safety basically means that the types - be a string, a number, boolean etc - are defined and any data that doesn’t fit the type will be rejected thus preventing type errors.
In most applications the underlying types will be the schema of your database, with each field having a fixed type. It is in the layers above the database where translation and magic occurs that lead to most typing issues.
Typically the largest translation layer is between the frontend and the backend. Be it a react app, a mobile app or some other client, talking back to the server requires the two speaking the same language.
These days my default is GraphQL which provides a rich, fully typed schema for all it’s operations. I won’t go into detail now on GQL as it is covered elsewhere in depth, but it is vital tool in achieving the typed-nirvana I’ve always dreamed of.
Most of my applications now use the following stack to achieve end-to-end type safety and I never have to worry about checking the calls between the layers as everything is strongly typed.
At a high level my process is as follows:
Define a data model in TypeORM
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export default class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@Column()
body: string;
}
Define the GraphQL types, via TyepGraphQL annotations on the same class (thus linking the types)
import { Field, ID, ObjectType } from "type-graphql";
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@ObjectType()
@Entity()
export default class Post {
@Field()
@PrimaryGeneratedColumn()
id: number;
@Field()
@Column()
title: string;
@Field()
@Column()
body: string;
}
Note: For something simple I would use the same classes for my ORM data model and my GraphQL types but for more sophisticated apps I will split these up and map data between the two (still type safe due to typescript!)
Create a resolver and define methods with a query annotation for TypeGraphQL to generate a GraphQL schema from
import {
Arg,
Mutation,
Query,
Resolver,
ResolverInterface,
} from "type-graphql";
import Post from "../entities/Post";
@Resolver((of) => Post)
export default class PostResolver implements ResolverInterface<Post> {
@Query(() => [Post])
async posts() {
// in reality you would call your database via TypeORM - dummy content for now
return [
{
id: 1,
title: "My post",
body: "Some content",
},
];
}
@Query(() => Post, { nullable: true })
async post(@Arg("id") id: number): Promise<Post | undefined> {
// in reality you would call your database via TypeORM - dummy content for now
return {
id: 1,
title: "My post",
body: "Some content",
};
}
}
Create the GraphQL server itself
import "reflect-metadata";
import { ApolloServer } from "apollo-server";
import { buildSchema } from "type-graphql";
import { createConnection } from "typeorm";
import PostResolver from "./resolvers/PostResolver";
async function init() {
// reads from ormconfig.json by default
await createConnection();
const schema = await buildSchema({
resolvers: [PostResolver], //reference the resolver
});
// Create GraphQL server
const server = new ApolloServer({
schema,
playground: true,
tracing: true,
introspection: true,
});
// Start the server
await server.listen(8000);
console.log(`Server is running on 8000`);
}
init();
Switching the frontend, setup GraphQL Code Generator to read you schema and output TS types Install the CLI and set the config as:
overwrite: true
schema: "http://localhost:8000/graphql"
documents: "src/**/*.graphql"
generates:
src/generated/graphql.tsx:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
config:
withHooks: true
This will generate a graphql.tsx
file which contains the generated type derived from your GraphQL schema as well as apollo-client queries and mutation methods (and hooks!)
Now in your react code you can require in the generated hooks which will provide fully typed inputs and results to method calls which are driven by your GraphQL server, which is generated from your database model:
import React from "react";
import { useCreatePostMutation, usePostsQuery } from "../../generated/graphql";
interface Props {}
function Home(props: Props) {
const posts = usePostsQuery(); //posts is now a Apollo client query result typed as Post
return (
<dive>
<table>
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Body</th>
</tr>
</thead>
<tbody>
{posts.data &&
posts.data.posts.map((post) => {
return (
<tr>
<td>{post.id}</td>
<td>{post.title}</td>
<td>{post.body}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}
This is a brief overview of how I approach things. If you are interested in more detail you can checkout a full implementatin in this repo on GitHub.
My more advanced implementations include other features such as:
Hit me up on Twitter - @alexolivier if you have any questions!