Phero: Build type-safe, full-stack apps with TypeScript
Tags: typescript, mobile
Published: 2022-12-29 15:15:52

**Written by [Yan Sun](https://blog.logrocket.com/author/yansun/)✏️**

Syncing backend API changes to the client app is a common problem frontend developers face. When the backend API interface is changed, the client app may need to be manually updated to prevent an error or crash. 

Phero is designed to solve this problem. Developed by [Jasper Haggenburg](https://github.com/Jpunt) and his team in 2020, it was initially created to manage types and interfaces for their software product, which consumes a large number of API endpoints. Phero is developed using TypeScript and works with any framework based on TypeScript. It will process the backend code and generate a type-safe client for the frontend. 

Using Phero, you can call the backend services/functions from the frontend just as you would call a local function. It's been open source since September 2022. Let's take a closer look at Phero. 

_Jump ahead:_

*   [What problem does Phero solve?](#what-problem-does-phero-solve)
*   [How does Phero work?](#how-does-phero-work)
    *   [Benefits of working with Phero](#benefits-working-phero)
*   [Getting started with Phero](#getting-started-phero)
    *   [Backend setup](#backend-setup)
    *   [Frontend setup](#frontend-setup)
    *   [Running the CLI to start the dev environment](#running-cli-start-dev-environment)
    *   [Creating a React client app](#creating-react-client-app)
*   [Error handling with Phero](#error-handling-phero)
*   [Deploying to production](#deploying-production)
*   [Migrating an existing app to use Phero](#migrating-existing-app-phero)

## What problem does Phero solve?

Let’s use a typical Node.js/Express web API as an example. We expose an endpoint like below:

```javascript
# API
export enum UserRole {
  ADMIN = "admin",
  USER = "user",
}
export interface User {
  name: string;
  age: number;
  role: UserRole;
}
export interface Users {
  [key: number]: User;
}
app.get("users/:userId", (req, res) => {
  res.send(
     {
      name: "Jane",
      age: 30,
      role: UserRole.USER
    });
});
```

In the client app, we need to create another set of type/interfaces and then cast the response JSON to get a typed response object.

```json
# Client App
export enum UserRole {
  ADMIN = "admin",
  USER = "user",
}

export interface User {
  name: string;
  age: number;
  role: UserRole;
}

const response = await fetch(`${Root_URL}/users/1`);
const user = await response.json() as User;
```

There are a few issues with this pattern. Firstly, there are duplicated types and interfaces on both the client and server sides. This duplication will make the app hard to maintain. 

Secondly, when the API changes (i.e., a new `UserType` is added), the client app may crash if it's not updated — and even if you did update the client, the older version of the client app will still have the same problem. Thus, both the server and client need to be updated at the same time, to ensure backward compatibility. 

To overcome the above issues, one common practice is to add a JSON response to the client code. But the extra validation requires additional work and is not safe enough because it can't catch issues at compile time. 

Phero provides a clean solution to this problem.

## How does Phero work?

Under the hood, Phero makes use of [remote procedure call (RPC)](https://blog.logrocket.com/introduction-to-rpc-using-go-and-node/). RPC is a communication protocol that allows a program on one machine to call a service on another machine without knowing it's remote. 

The below diagram shows the Phero client and service communication using RPC.

![A diagram illustrating the connection between Phero and the client via RPC](https://blog.logrocket.com/wp-content/uploads/2022/12/phero-client-connection-rpc.png) 

You can use any TypeScript-based framework to build your backend and frontend; otherwise, a Phero-enabled app comprises three components:

1.  The client app: Your client app just needs to consume the generated Phero client file when interacting with the server
2.  The server app: The entry point of the server component is `Phero.ts`; you also have the flexibility to organize the server project structure as long as the Phero file exposes the functions
3.  Phero-generated code: The generated files are the glue between the backend and frontend, and include the client-side stub and server-side stub files. The stubs enable the server and client to communicate via RPC, using the HTTP protocol

Phero makes use of the TypeScript compiler API to analyze types and interfaces. It also generates the validator to parse the incoming and outgoing data between the client and server. It exposes all the domain types and interfaces exposed by the backend; thus, a compile-time error will be thrown if a typo or incorrect data is returned.

### Benefits of working with Phero <a name="benefits-working-phero">

The benefits of Phero include:

*   Pure TypeScript (no other dependencies required)
*   End-to-end type safety
*   Separate backend and frontend that act like they are not separated
*   Work with any TypeScript-based framework

## Getting started with Phero <a name="getting-started-phero">

Let's walk through the process of building a Phero app. Since Phero is a pure TypeScript library, the only dependencies are npm and TypeScript.

### Backend setup <a name="backend-setup">

Firstly, let’s initialize a Node.js app. We create an `api` directory below. It can be named anything you wish.

```bash
# bash script
#  Create a Server folder
mkdir api
cd ./api
# Initialise the server app, and get TypeScript ready 
npm init -y
npm install typescript --save-dev
npx tsc --init

​# edit tsconfig.json file to add the following entries
# it is a temporary workaround until the bug is fixed in Phero CLI
{
 "compilerOptions": {
   ...
   "rootDir": "src",
   "outDir": "dist"
 }
}
# Add Phero to your backend project
npx phero init server
```

After the above commands are completed successfully, you will have a new `phero.ts` file in the `api/src` folder.

```typescript
# api/src/phero.ts
import { createService } from '@phero/server'

async function helloWorld(name: string): Promise<string> {
  return `Hi there, ${name}!`
}

export const helloWorldService = createService({
  helloWorld
})
```

The generated `Phero.ts` file exposes a `helloWorld` service and is ready to be consumed by the client.

### Frontend setup <a name="frontend-setup">

Let's run the following script to set up the client. We create an `app` subfolder, but it can be named something else. The Phero CLI can determine whether the folder is for the client or server by scanning the `phero.ts`.

```bash
# bash script 
# from your project root directory, create a app subfolder 
mkdir app
cd ../app

# initialise a node app
npm init -y
npm install typescript --save-dev
npx tsc --init

# Add Phero to your frontend project:
npx phero init client
```

The `phero init client` command installs the `@phero/client` and generates a `Phero.ts` file.

```typescript
# app/src/Phero.ts
import { PheroClient } from "./phero.generated";

const fetch = window.fetch.bind(this);
const client = new PheroClient(fetch);

async function main() {
  const message = await client.helloWorldService.helloWorld('Jim')
  console.log(message) // `Hi there, Jim!`
}

main()
```

### Run the CLI to start the dev environment <a name="running-cli-start-dev-environment">


Now we can run the Phero CLI to start the development environment.

```bash
# back to the project root directory
cd ../
npx phero
```

The above command starts the server and generates an SDK for the frontend (or multiple frontends) at the same time. You should see the following output in the console: [caption id="attachment_146409" align="aligncenter" width="730"]![The output of the Phero SDK installation in the console](https://blog.logrocket.com/wp-content/uploads/2022/12/console-output-phero-sdk.png)

In the frontend project, a `phero.generated.ts` file will be generated. It's the RPC client stub used to connect to the server stub. As the name indicates, it shouldn't be manually updated. It's automatically synced with any server-side change when the CLI is running. 

Although the basic setup is completed now, we can't run the client to prove that it's working because we don't have a proper client app to run in a browser yet. 

Let's build a React client app to test it end-to-end.

### Creating a React client app <a name="creating-react-client-app">

Run the following command to generate a skeleton of a React app.

```bash
npx create-react-app react-app --template typescript
```

Then, you can start the app as below.

```bash
cd react-app
npm start
```

Now we have a working React app, so the next step is to add Phero to it.

```bash
# bash script
npx phero init client
cd .. # Back to project root directory
# run Phero CLI to start the dev environment
npx phero
```

The above command will generate the `phero.generated.ts` file in the React project. 

The last step is to open the `src/App.tsx` and replace the original code with the following code snippet:

```typescript
import { PheroClient } from './phero.generated';
import { useCallback, useEffect } from "react";

const phero = new PheroClient(window.fetch.bind(this))

function App() {
  const getHello = useCallback(async () => {
      const message = await phero.helloWorldService.helloWorld('Jim');
      console.log(message);
  }, [])

  useEffect(() => {
    getHello()
  }, [getHello])
```

The above code snippet will initialize a `PheroClient`, and makes a call to the `helloWorldService.helloWorld` method. 

To verify the app is working, open `[http://localhost:3000/](http://localhost:3000/)` and open [Chrome DevTools](https://blog.logrocket.com/debugging-node-js-chrome-devtools/). You can see the console message and the client-to-server RPC call in the **Network** tab. [caption id="attachment_146414" align="aligncenter" width="730"]![The client-server RPC call as shown in the Network tab](https://blog.logrocket.com/wp-content/uploads/2022/12/client-server-rpc-call-network-tab.png)

You can find the related source code in [the GitHub repo](https://github.com/sunnyy02/PheroDemo).

## Error handling with Phero <a name="error-handling-phero">

Using Phero, we can handle the custom server-side error in our frontend just like we would a local function. 

Let’s keep going with the previous API example. if a user can't be found by their given user ID, we want to throw a `UserNotExistException` error on the backend.

```typescript
class UserNotExistException extends Error {
  constructor(userId: number) {
    super()
  }
}
app.get("users/:userId", (req, res) => {
    const user = db.getById(userId);
    if(user === null) {
      throw new UserNotExistException(userId);
    }
    return user;    
});
```

Then at the frontend, we can handle the error as below. Please note that the `UserNotExistException` is exposed in the client by the generated Phero file.

```typescript
import {
 PheroClient,
 UserNotExistException,
} from "./phero.generated"

const fetch = window.fetch.bind(this)
const client = new PheroClient(fetch)
 try {
   const user = await client.userService.getUser(1)
 } catch (error) {
   if (error instanceof UserNotExistException ) {
     console.log("User not found")
   } else {
     console.log("Unexpected error")
   }
 }
```

Again, we gain type-safety in error handling with Phero.

## Deploying to production <a name="deploying-production">

At the local development environment, the default Phero server API runs at `port 3030`. When deploying to a higher environment or production, you may need to use a different port number. Same as any Node.js API, the port number is configurable by passing in an environment variable `PORT` at runtime or using a `.env` file. 

For example, to run the server API at `port 8080`:

```bash
cd ./.build
PORT=8080 node index.js
```

In a non-local environment, you may also need to make the API URl configurable on the client side. We can store the URl in an environment variable and pass the URL to `PheroClient` as below.

```typescript
import { PheroClient } from "./phero.generated"
const client = new PheroClient(fetch, `${API_ROOT}`)
```

To build a bundle of server code for deployment, run the command below.

```bash
cd ./server
npx phero server export
```

This will generate the bundle files into a `.build` directory. The server can be started with a standard Node.js command. 

At the moment, the Phero server can run on any Node-based server. According to [@kamil](https://github.com/kamilafsar) from the Phero team, they plan to provide new export and deploy features for different cloud platforms.

## Migrating an existing app to use Phero <a name="migrating-existing-app-phero">

If you want to migrate an existing server app to use Phero, you may want to restructure your Phero app first. 

Phero doesn't come with lots of restrictions. You just need to define the `Phero.ts` as the entry point of the backend app and export your services from the Phero file. You can design your services and functions using any TypeScript-based framework. 

Let's say we need to migrate a Node.js/Express API app to Phero. We need to define clear "groups" of functions that need to be called from the frontend. In Express, you would typically group them in "`Routers`". We can covert these groups into services in Phero. All routes you currently have inside these routers can be migrated to regular TypeScript functions. 

As an example, this function:

```typescript
router.post('/example-route', (req, res) => {
  const data: MyData = req.body

  res.send({ output: 123 })
})
```

Would become the following after router migration:

```typescript
async function exampleFunction(data: MyData): Promise<{ output: number }> {
  return { output: 123 }
}
```

When designing the new Phero service interface, you should think about the types. If you’re only using primitive types like string and number, the type safety that comes with Phero is probably overkill for you. Defining well-constrained types in the Phero backend will help you detect errors in compile time at the frontend.

## Summary

Phero is a type-safe glue between the backend and frontend. It's a viable solution for ensuring better type safety in frontend development. Using Phero, you can call backend functions or handle backend errors from the frontend with end-to-end type safety. It will help you to write more concise code and have peace of mind in refactoring. 

I hope you find this article useful. Happy coding!

---
## [LogRocket](https://lp.logrocket.com/blg/typescript-signup): Full visibility into your web and mobile apps

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/typescript-signup)

[LogRocket](https://lp.logrocket.com/blg/typescript-signup) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

[Try it for free](https://lp.logrocket.com/blg/typescript-signup).