Flow-based API design with Typescript

Vasilyev Maksim
Frontend Weekly
Published in
3 min readDec 8, 2020

This article is also available in russian.

Photo by Alvaro Reyes on Unsplash

Hi! ✋

I want to talk a little bit about how we design service classes. By “service class” I mean a class containing business logic and/or server communication logic. Let’s take a look at a simple but demonstrative example.

Let’s imagine that we develop command line application (for the sake of simplicity) for login process using typescript. Also, we need to implement two-factor authentication via SMS and change expired password functionality.

Here is the user flow:

User flow

What can an authentication service look like? The first thing coming to our mind is:

Where did such an API come from? The answer is obvious: each request to the server is represented with a single class method.

Let’s look at how we can use this service:

But what’s wrong 🤔?

What if you are a newcomer? You see the code above for the first time and you have never seen the user flow (at the beginning of the article). Can you tell for sure what is the right order you should call service methods in? Nope 😐

For example, it makes no sense to call submitOtp before login, or call changeExpiredPassword if the password has not expired yet. These are restrictions IAuthService interface declaration tells us nothing about.

So what should we do then? Let’s try to write IAuthService in a different way. To do this we will use super handy and one of my favorite typescript features called discriminated unions.

Note: If you want to know more about discriminated unions, why is it so useful, and learn some real-world use cases, I advise you to take a look at these articles:

So, let’s use discriminated unions and see what we will get:

Nice 👍! By design, only login is initially available. Then, submitOtp or changeExpiredPassword becomes available depending on whether the password has expired (otpSentTo is needed in both cases). In the end, only login is available after changeExpiredPassword.

We always get the API that is relevant at the moment. Nothing else. User flow dictates the design of the API and can be unambiguously restored from it.

Indeed, the user flow diagram can be easily restored from the declaration of IAuthService.

Here is an example of IAuthService implementation for clarity:

And usage:

I would like to bring another small example.

Let’s consider login flow powered by SRP protocol. Without going deep into details: SRP protocol implies 2 requests from client to server (and a lot of stuff with hashes, keys, etc.). Nevertheless, only one user interaction is needed — username and password entry. The UI shouldn’t orchestrate low-level API calls to the SRP server. All the magic with multiple requests, hashes, keys, and salts should be hidden inside a single login(username, password) method, which returns a promise that resolves only after the entire SRP process ends. Ave encapsulation 💪

Think about the code consumer when you design a service API. The consumer can be a react application, a command line application, etc. Interaction with the service should be minimal, obvious, and as convenient as possible. And of course, service shouldn’t be just a dumb retelling of the client-server contract.

Design beautiful services! Thanks for your attention 😊

Feel free to reach out to me if you have any questions or would like to connect, either through Twitter, LinkedIn, or just plain old email: vasilyev.maksim93@gmail.com

--

--