Sharing TypeScript Types Between Backend and Frontend

If you are not using a mono repository, then the communication between client and server with a common data model will be a problem. Without maintenance, duplicate code will lead to desynchronization.

If the data model changes on the server side, make sure that the client code picks up these changes. Otherwise, the client code will suffer greatly when receiving incompatible data, or due to ignoring additions. This is not what you want.

Let’s take a look at strategies for keeping client and server code in sync.

Extracting shared models to third-party dependencies

Let’s say you are working with multiple repositories. After defining the data model (set of data models) that the server and its clients use (custom applications or other services), it is a good idea to extract the models into a shared library.

In this way, you ensure that the server and client import and use the same data model.

What are the advantages of this? The server and client will know exactly what they are sending to each other. And they will be aware of any changes at the same time. You have essentially aligned the play area for both sides.

Wonderful.

Then what is wrong?

Let’s say we are working on a JavaScript-Node.js application. Most likely, you will have to turn the new repository into an NPM package. It is not difficult, but it will take extra time. Most importantly, you will create a new repository. But who is responsible for it? Backend? If you need to make a change to the model for one of the client projects, what is the best way to do it? When working in a multi-project environment with multiple teams (a common scenario), your actions can create an organizational nightmare.

This solution is suitable for those scenarios where one team works on the entire project and owns the entire codebase.

A warning: make sure to move to a third-party DTO (Data Transfer Object) module that contains only the fields used to transfer data between entities. Since a DTO can be a class, it is tempting to share methods that include private business logic. This is unacceptable, so be careful.

It’s one thing to share multiple fields, and it’s another to share business logic that is potentially confidential to the company.

Sharing Types with TypeScript and Bit

https://bit.dev/deleteman/quotes-lister

If you are using TypeScript, make sure to share not only the “shape” of the data, but also the types.

In this example, we’ll use Bit instead of creating a new repository and turning it into an NPM package.

Bit is an open source tool (with built-in integration and remote hosting platform Bit Cloud). Bit helps build and share independent components. These components (modules) are developed independently, versioned and shared.

Create new independent components from scratch, or pull them out gradually from an existing codebase (this is just our case).

This approach is similar to NPM, but there are differences:

  • For versioning, you don’t have to manually checkout, share, and work on it separately. You can “export” a component from a repository. Bit identifies a piece of code as a component and handles it independently. This simplifies the sharing process, does not require creating a separate repository, and removes the need to redesign the way these files are imported into your project.

  • People who “import” your components (rather than install them) can collaborate with them, modify and export them back to their “remote area” (remote hosting of the components).

This approach is very effective because it makes it possible to share the same tool among different teams within the same organization. However, you do not need to create a separate project.

When importing the Bit component, the code is loaded and copied to the working directory. At the same time, it creates the corresponding package in the node_modules directory. When you change the source code (in the working directory), the package is rebuilt. So you are using this component with an absolute path. This works in any context, and will work even if the component package is installed without importing.

It’s our goal to share the local setup in a multi-repository environment without worrying about problems.

I say “multi-repository” in quotes because we don’t care about creating and maintaining a new repository. You can turn some of the base code into an independent component, version it via Bit, and publish it to a centralized repository.

After that, the new component with generic types can be imported anywhere. This means that any client application or microservice will import the type as an external dependency. And we don’t need any manipulation of NPM or Github for that.

Finally, if you only publish type definitions, you don’t have to worry about protecting your business logic.

Example

Given how easy it is to create a working example with Bit, I’ve prepared a simple project to demonstrate.

The project is called “Quote Lister”, you can see it here or directly on Github… The logic is very simple:

  • Service in Node.js, serving a list of quotes taken from a text file.

  • The React component asks for these quotes and displays them on the screen.

As you might have guessed, both components can use a common type to describe the structure of quotes. Create a new component that only exports generic types. I will have both a service and a react component importing it.

The details of the project implementation for the Backend and Frontend are virtually irrelevant. Just look at the screenshot with the most interesting parts of the client and server.

Frontend
Frontend
Backend
Backend

Here’s a React component and a Node.js backend. They are different codebases, but they install the same library: @deleteman/quotes-lister.shared-types

And here’s the interesting thing: this is not an external library, but it can be used that way thanks to the magic of Bit.

Let’s look at the project structure:

Project structure
Project structure

All this is local and all components are managed by Bit, and links to them are generated automatically from the node_modules folder under my username Bit (@deleteman). This way the projects use a shared library, and we don’t have to worry about publishing to NPM and supporting another repository. If we only worked with one or two projects and the code was more general, then you could just install them through Bit or NPM separately.

This is what the created module looks like on the Bit platform
This is what the created module looks like on the Bit platform

BFF (Backend for Frontend) pattern

When synchronizing client and server code, we will share this code. This task can be simplified using the BFF pattern. True, this solution also has its drawbacks.

The BFF pattern is a way to create a common interface between server and client without sharing code between projects. How to do it? Create a proxy service that acts as a mapper between both data models.

This avoids problems when sharing external dependencies or types between client and server. And to solve the problem of standardization, we can add additional services.

This is a good decision? It all depends on the circumstances. If you have a single client, you can use this pattern to give both parties more flexibility in defining data types.

On the other hand, if you have multiple clients, then this approach will help you change versions of services with minimal impact on clients. You add a proxy and update the service version while maintaining backward compatibility. Proxies can be used to map data changes in the new version. First, test the new version of the service and make sure it works. Then you can ask customers to update the code.

With the BFF pattern, you can synchronize API code and client code while leaving them out of sync.

Risk – sharing too much

Finally, advice on the problem we are trying to solve.

If we adhere to the DRY principle, then when synchronizing the client and server code, there are two main problems:

  1. You may be sharing more data than you should. We have already discussed this: you are sharing the entire class and its methods, instead of sharing the basic data definition (names and types of fields). Sharing details makes sense if method logic can be used in both places. Otherwise, you expose private business logic to the client. And if the client is an external party (and not another application in your organization), you pass the IP address of your company. Do I need to continue?

  2. The more code you share, the more interconnections arise between systems. It’s counterintuitive, but every effort you make to share your code and keep from copying and pasting files between projects forces you to tie the server and client implementation together. Remember, after implementing the first version of the project, the minute the server needs to make changes to the model, you need to make sure that all clients are up to date.

How can you avoid these problems? The first and easy way: refrain from sharing code as part of data models. If there is a shared code that needs to be shared, please do so elsewhere.

As for the second problem, there is simply no solution. The question is more of an acceptance: given the situation, is the communication between the client and the server a problem? What to do next depends on your answer.


Having a single data model between client and server is a common situation. And there is no right answer as to how to approach it. Somehow share the code, copy and paste files, or create another service for mapping them.

Choosing the best solution is entirely up to you and your context.


Translated for you by Nikita Ulshin, Team Lead & JS-developer, blogging ulshin.me and TG channel @ulshinblog

Comments, suggestions and constructive criticism are welcome 🙂

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *