sleepy

TOPICS

IntroductionReact (with MobX)ArchitectureModelsNetworkingData TransformersStoresViews & View Models

Networking

Responsibility

This layer makes network calls and returns the response. It contains no logic whatsoever.

Description - Why do we abstract networking?

One thing I've seen in a lot of apps is how much the networking layer "bleeds" into the rest of the app. When we don't abstract our networking layer, mocking out our networking layer for tests or local development becomes difficult. I like to keep my networking code completely decoupled from the rest of the app and injected as a dependency abstracted by an interface (this is the dependency inversion principle). Let's go over an example

Weather Example

Let's build an app that can take a zip code, or the user's current location from the browser, and display the temperature for that location. We'll use Open Weather for this, and start with the current weather call.

The response object is has a dictionary called main that includes a temp in Kelvin, and a property called name with the name of the location. We want to decouple ourselves from the actual API calls and translate this into the model our app uses so that we can break our app into differen slices and be able to easily write tests, quickly switch to a local mock network, or decouple our UI/the rest of the app from a networker if using a tool like Storybook.

The first thing I like to do is set up a Promise type with the data we expect back from an API response. This is essentially a subset of what axios provides, but I abstract it so that 1) I'm not bleeding my networking library (Axios) into the rest of the app and 2) I can swap axios for something different if I ever need to, and I only would need to update the types here.

Of all the things in the Axios response, I really only care about the data and status, so I'm going to make an interface based on that.

1 2 3 4 type NetworkResponse<T> = { data: T; // The data object we expect status: number; // The status code of the response };

While this type is nice, all of our network responses will be of type Promise<NetworkResponse<T>>. So to simplify this a bit, I'll create another type that combines them

1 type NetworkPromise<T> = Promise<NetworkResponse<T>>;

If this is confusing, hopefully it'll get a bit clearer when we define our first network call!

Say we'd like to make a call to Open Weather to get some weather data. Our app is going to display the current temperature for a zip code. Based on the response JSON, we can create a type in our app that matches the response like this:

1 2 3 4 5 6 7 // Based on https://openweathermap.org/current#current_JSON type CurrentWeatherResponse = { name: string; main: { temp: number; }; };

We probably also will want a type check to make sure the data is in the format we expect. This would look like this

1 2 3 4 export const isCurrentWeatherResponse = (object: unknown): object is CurrentWeatherResponse => { const response = object as CurrentWeatherResponse; return !!response && !!response.main && !!(typeof response.main.temp === "number") && !!response.name; };

The purpose of the type check on the response is to verify the response is in the format we expect. Our app will have issues if we've named a property incorrectly or expected a string when we got a number.

Let's create an interface for our networking layer to conform to in order to get this current weather response. We know the API takes a zip code and get the current weather, so it should look like this

1 2 3 interface OpenWeatherNetworkable { getWeatherForZip(params: { zip: string }): NetworkPromise<CurrentWeatherResponse>; }
1 2 3 4 5 class Networker implements OpenWeatherNetworkable { getWeatherForZip(params: {zip: string}): NetworkPromise<Weather> { // Make network call }, }

I use axios on my web projects, but the beauty of this pattern is you can use whatever you want since it's all encapsulated in one file! Create these network requests however best suits you

1 2 3 getWeatherForZip(params: {zip: string}): NetworkPromise<CurrentWeatherResponse> { return axios.get(`${this.baseUrl}/weather`, {params: {...params, appid: this.apiKey}}); }

We also know that we're going to get weather for a location, so we can set that up now as well based on the Open Weather API. Note that the params here correlate directly to the Open Weather API.

1 2 3 4 interface OpenWeatherNetworkable { getWeatherForZip(params: { zip: string }): NetworkPromise<CurrentWeatherResponse>; getWeatherForCoordinates(params: { lon: number; lat: number }): NetworkPromise<CurrentWeatherResponse>; }

And we will fill in the rest of the networker

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class OpenWeatherNetworker implements OpenWeatherNetworkable { private readonly apiKey: string; private readonly baseUrl: string; constructor( baseUrl: string = "https://api.openweathermap.org/data/2.5", apiKey: string = process.env.REACT_APP_OPEN_WEATHER_API_KEY ?? "" ) { this.baseUrl = baseUrl; this.apiKey = apiKey; } getWeatherForZip(params: { zip: string }): NetworkPromise<CurrentWeatherResponse> { return axios.get(`${this.baseUrl}/weather`, { params: { ...params, appid: this.apiKey } }); } getWeatherForCoordinates(params: { lon: number; lat: number }): NetworkPromise<CurrentWeatherResponse> { return axios.get(`${this.baseUrl}/weather`, { params: { ...params, appid: this.apiKey } }); } } export default OpenWeatherNetworker;

What's Next?

Next we'll move onto a data transformer to type check and translate our response into what the app will use!