Managing Mugs: Decoupling Frontend and Backend Development

How can the frontend team continue developing new features, without being blocked by the backend? We need a way to decouple the teams.

Introduction

Imagine: you just started working on a new project for a client as a Flutter developer. As it is a rather large project, the client hired a dedicated frontend team and a dedicated backend team. As you are working with Flutter, within weeks, the frontend team is miles ahead of the backend team in terms of features. Only later will the frontend team be tasked with frontend-heavy features, allowing the backend team to catch up. How can the frontend team continue developing new features without being blocked by the backend? We need a way to decouple the teams. This blog will describe API specifications, mocking backends using imposter, and end-to-end tests. And what better way to illustrate my point than a little case study?

Case Study: Managing Mugs

To illustrate the steps that we will take, I require a case study. Here at Baseflow, everyone has a personalized mug. You can tell my mug apart from the other mugs because mine uniquely has my name on it. We will create an application for this blog to manage mug ’objects.’ I made a GitHub repository containing the Mug Manager application we are about to build athttps://github.com/Baseflow/mug-manager.

API Specifications

Before the backend team starts implementing a feature, writing the API (Application Programming Interface) specifications is good practice. These specs act as a contract between the frontend and backend and dictate what is sent and received between the application and the server. Including frontend and backend developers in this process is valuable so they can weigh in on considerations from both ends. By writing out the specifications, either team can start working on the feature as they know which data they will receive and send.

OpenAPI is an industry standard for writing APIs. An online editor can be found at https://editor.swagger.io/. In OpenAPI, you can specify your endpoints and show example requests and responses. Let’s get to work on our mug manager. We leave authentication out of the equation and assume that users are somehow authenticated. We start by defining the Mug model. Mugs at Baseflow consist of a first name, last name, and optional nickname. Sometimes, a colleague drops a mug when clearing out the dishwasher, breaking it. Therefore we would like to store a boolean to indicate whether the mug is broken. We also give mugs a unique identifier. We put this information in the OpenAPI spec.

Mug model in OpenAPI

Now that we have our mug model let’s define some endpoints. We define GET /mug to read all mugs from a database, POST /mug to create a new mug, PUT /mug/{mugId} to update an existing mug, and DELETE /mug/{mugId} to delete a mug. We can define route parameters, request body, and responses.

Backend routes in OpenAPI
PUT route information for updating mugs

The OpenAPI spec can be found in the GitHub repository at /imposter/mug.yaml.

Implementing a Mock Server Using Imposter

Now that the contract has been established as an OpenAPI specification, the backend and frontend teams can work on the feature separately. The backend team could even decide to skip this feature for now and focus on something else. Great! But once the frontend team has finished their implementation, they want to test and demo their work. The most straightforward approach would be to write a bit of code that manages data in memory. But if they're going to do it properly, they could also implement a mock server. A mock server is a separate program that pretends to be the backend. For our mug manager, we will use imposter (https://www.imposter.sh/). The imposter can generate a mock backend from the OpenAPI spec we created! We create a config file pointing imposter to the specs.

Now we can start imposter using the Imposter CLI by running imposter up -p 8080. A backend server will be created on your machine at localhost:8080. For now, imposter will output default responses without keeping the state. To quickly test our implementation, we will use Postman (https://www.postman.com/). Postman allows us to talk to the backend easily.

Firing POST from Postman to Imposter

We want more, however. The mock backend should also use some form of in-memory storage so we can create, update and delete mugs during a demo as if there was an actual backend. Luckily, imposter gives us fine-grained control with unlimited possibilities as it allows scripting. By providing Groovy or Javascript code, we can customize imposter in any way we want.

Scripting Imposter to generate a mug identifier on creation

Using Postman, we test the POST route again. The response from imposter contains HTTP status code 200 (OK) and the created mug as content. We can see that the script generated the id, indicating that responses are no longer static.

Firing POST from Postman to updated Imposter

All Imposter codes can be found under/imposter. As we finish the mock backend, it is time to connect to it in Flutter!

Talking to the Backend in Flutter

When implementing a data layer, it can be essential to make a clear distinction between core layer code and data layer code. We want to communicate to a backend over HTTP for this case study. But, in the future, we might want to communicate with a local caching service that occasionally sends HTTP requests. Or we might want to store all the data locally. The code in the core layer will contain the mug model and related business logic.

In contrast, the data layer implements data layer-specific functionality, such as converting mugs to and from JSON. The core code will contain a Mug model and a Mug repository. The Mug model includes all fields of a mug. First name, last name, nickname, whether it is broken, and an id. The repository is an interface that declares methods such as createMug(), updateMug(), and deleteMug(). Then, in the data layer, we create specific HTTP instantiations of these classes. We make an HttpMug model that contains logic to translate the mug object to and from JSON so that we can send it over the web. We also create an HttpMugRepository that uses an HTTP client to send and receive data from a backend. Our code becomes loosely coupled by clearly splitting the core and data layers. We can quickly implement a different data layer in the future that will not affect the core code, and we can test business logic and data-specific features separately.

MugRepository in the core layer
HttpMugRepository in the data layer

With the Flutter application finished, we can now demo it. We will launch Imposter, followed by the app.

Mug Manager Flutter application on an iOS emulator

All Flutter-related code can be found in the repository under/lib.

End-to-End Testing

Now that we have both the Imposter backend and a working application, we can write end-to-end tests. End-to-end tests are the closest to a real-world interaction with an app. You can view them as a person opening your app and using it. Contrary to unit tests that test only small, contained pieces of code, end-to-end tests start an instance of the application, interact with the user interface and check whether the app behaves as expected. For end-to-end tests, all services used by the application should be available. Usually, this includes a backend. It is bad practice to use the actual backend during tests. Tests will be slow due to communication over the internet, and backend calls may incur financial costs.

Additionally, the database keeps state, which is not ideal when testing for deterministic behavior. As our app requires a backend, nonetheless, using the mock backend instead is perfect. We can run it locally before running the tests. It is quick, costless, and can easily be wiped clean.

You could automate the starting Imposter before running the tests, but we will not go into that here. Our tests will assume that Imposter is up and running before testing if users can create, update and delete mugs.

End-to-end test for creating a mug

The end-to-end tests can be found in the repository at /integration_test/app_test.dart.

Outro

In this blog, we developed a mug manager application to demonstrate how we can decouple frontend and backend work as much as possible. We created an OpenAPI spec and a mock backend. Aside from decoupling, we also made end-to-end tests by using the mock backend as a drop-in replacement for an actual backend.