End to End Typesafe Application
Tags: typescript, node, typesafety, javascript
Published: 2022-10-29 12:41:15

---
title: End to End Typesafe Application
published: true
description: Typesafety in a web application, why it's important, and how to archieve it !
tags: Typescript, Node, typesafety, javascript
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/on0ss2f0pg14iug46rn0.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2022-10-29 14:41 +0000
---

## Typesafety
When working in a javascript environment, adding type safety to an application may be challenging. Of course we have typescript that makes the developper experience smoother but it is mostly usefull at **build time** and during **developement**, what about **runtime**?

Through this post i'll try to go over on how we can achieve this to have the best DX possible and help teams from back-end to front-end to share a common knowledge and avoid frictions.

## JS Ecosystem
This has already been addressed by several communities, we can build our own tool or use existing tools like **[Yup](https://github.com/jquense/yup)** / **[Zod](https://github.com/colinhacks/zod)** that are schema validator ! 
Most of the time i've seen teams using it only in the front end for Form Validation, but taking a step back and looking at the possibility it offers, with the right implementation it allows us to have an **E2E type safe** applications.

## Why use it ?
Having a typescript with types shared across an organization is really nice and give us more confidence in our codebase, but what if we want to control the data flow, have some decent alerting on **why** something failed on the client, **where**, and **what** was missing ? 
Except from the stack trace when it crashes and we receive the error on sentry or some other alerting tool, it may be challenging ! 
What if on every data used on an application, everything was controlled, and when something fail our reporting tool would be capable to tell us exactly what failed and why and where ? 

In my team we went from hundreds of daily errors / warning due to flaky types or things that were not supposed to happens because of a bad input to only few warning that we can tackle fast and efficiently.

## Strucutre
Depending on your organization structure, there is several ways to implement it, i'll go through the most common ones, every team is different and have their own structure, so i'll write here about the polyrepo / monorepo approach.

### The monorepo approach
Within an organization if the code if shared in a monorepo, creating a shared package within it, may be a handy solution.
You can then use it across your services and applications through an install command depending on the tool you're using (yarn, npm, lerna, nx etc...)

### The polyrepo approach
If you're using multiple repositories in your organization you can publish your shared type safe schema on your private / public npm registry, and then use it in any repository that may be relevant. 

## Usage
*Note: this works regardless of the framework or library you use, it is just a plain javascript library that is agnostic*.

I'll be focusing on **Zod** but the idea is the same regardless of the library. 
Let's take the example of an API that can edit a USER. 

#### Shared
```typescript
// In my schema folder
import z from 'zod';

export const ZUser = z.object({
  age: z.number(),
  createdAt: z.date(),
  id: z.string().min(1),
  email: z.string().email(),
  firstname: z.string().min(1),
  lastname: z.string().min(1),
  updatedAt: z.date(),
});

export const ZUserEditSchema = ZUser.omit({ createdAt: true, updatedAt: true, id: true });
export const ZUserIdParam = ZUser.pick({ id: true });

export type UserSchema = z.infer<typeof ZUser>;
export type UserEditSchema = z.infer<typeof ZUserEditSchema>;
export type UserIdParam = z.infer<typeof ZUserIdParam>;
```
So what is going on here ? 
With Zod we defined our schema !
Then we infer the schema definition to a typescript definition through the method [`z.infer`](https://github.com/colinhacks/zod#type-inference).

#### Backend
```typescript
import { ZodError } from 'zod';
import { UserIdParam, ZUser, ZUserIdParam } from '@mylib/schema';


app.put(
  '/user/{id}',
  async (
    req: Request<UserIdParam, UserEditSchema>,
    res: Response<{ outcome: string; reason?: string; error?: unknown; entity?: UserSchema }>,
  ) => {
    try {
      const safeParam = ZUserIdParam.parse(req.query);
      const safeUser = ZUserEditSchema.parse(req.body);
      const user = await updateUser(safeParam.id, safeUser);

      const safeUpdatedUser = ZUser.parse(user);

      return res.send({
        outcome: 'found',
        entity: safeUpdatedUser,
      });
    } catch (err) {
      reportToMyTool(err);

      res
        .send({
          outcome: 'notFound',
          reason: 'schemaValidationFailed',
          error: err as ZodError,
        })
        .status(404);
    }
  },
);
```
In our API, we import the schema definition and the typescript definition that we then use to type our api interface and we leverage zod to **validate** our data. 

There are among others 2 main usefull methods for it, here i am using the [`parse`](https://github.com/colinhacks/zod#parse) method that basically will throw an error if some types from the schema does not match. 
Another common one is the [`safeParse`](https://github.com/colinhacks/zod#safeparse) that wont throw and let you handle as you want the types that failed.

So what we know here is that **if** the id or request body does not match  the correct schema definition or if the user that we retrieve from the database also fail to match our schema it will throw an **error**!
Great ! What about the client side ?

#### Client
```tsx
// request
async function updateUser(user: UserEditSchema, id: UserIdParam['id']) {
  try {
    const response = await fetch(`https://myendpoint/user/${id}`, { method: 'put' });
    return await response.json();
  } catch (err) {
    reportToMyTool(err);
  }
}

// Component
export const UpdateUserForm = ({ id }: { id: string }) => {
  const [user, setUser] = useState<Partial<UserEditSchema> | null>(null);

  // parse the user object before trying to make a request
  const onUpdateUserClick = useCallback(async () => {
    const safeUser = ZUserEditSchema.safeParse(user);
    if (safeUser.success) {
      await updateUser(user, id);
    } else {
      toast({
        type: 'warning',
        description: safeUser.error.errors.map((error) => error.message).join('\n'),
      });
    }
  }, [id, user]);

  const onChange = useCallback(
    (event: ChangeEvent<HTMLInputElement>) => {
      const { name, value } = event.target;
      setUser({
        ...user,
        [name]: value,
      });
    },
    [user],
  );

  const inputs = ['email', 'firstname', 'lastname', 'age'];

  return (
    <form>
      {inputs.map((name) => (
        <input name={name} key={name} onChange={onChange} />
      ))}
      <button onClick={onUpdateUserClick}>Submit</button>
    </form>
  );
};
```

Here we defined a component named `UpdateUserForm`, it has some inputs and a button to make a request to update the form. 
In the function attached to the button, it will first parse our `user` object, if all the required data are ok it will then trigger a request on our backend, otherwise it will show a message to the end user notifying them on what went wrong and which field is missing.

## Last thoughts
This example just go through backend + front end communication, but you can also use it on your client side data, like states and others... !

I hope you enjoyed the article, this is my first time going through this, would love feedbacks and comments ! 
If you liked it, hit me up, i'll be thinking about the next subject to talk about ! 

Cheers !