eralp.dev

moon indicating dark mode
sun indicating light mode

Mocking APIs using embedded web servers inside flutter apps

May 03, 2022

(Edit 23.05.2022) I gave a tech talk about this at Flutter Nordics Helsinki Meetup, you can watch it from here: https://youtu.be/pj-ZTEnonE0

Say you are working on an app UI. But required APIs are not ready. Yet you need to deliver the app UI implementation as complete as possible. You could;

  • Hardcode API responses into app code
  • Use something like mockito and work with tests
  • Create a web server app and run it separately.
  • Embed a web server inside the flutter app itself.

I ended up taking the last option. Skip to it below if the rest of the options seem apparent. Each option has its pros and cons, so;

Hardcoding API responses

Pros

  • Easy.
  • App UI behaves as expected.
  • Everything can be done in the same codebase.
  • No need to have a server running.

Cons

  • The code is not final
  • Difficult to test HTTP errors
  • Difficult to test race conditions
  • Difficult to test serialization / deserialization

Developing with mocks/stubs in tests

Pros

  • Means that you write tests, nice.
  • Ability to test HTTP errors
  • Ability to test serialization/deserialization
  • Mocks are in the same codebase as the app.

Cons

  • Doesn’t work in end-user UI.
  • End-users can’t test with it.
  • Need to run the tests to work with them.
  • Need to write and set up tests (not too bad anyway)

Create a web server app and run it separately

Pros

  • Having an actual web server close as it gets to real API implementation
  • Ability to test everything related to HTTP and REST
  • Ability to test HTTP errors
  • Ability to test JSON serialization / deserialization

Cons

  • Need to create the web server as a separate project. If you made it in nodejs, you would need to run it separately.
  • Need to figure out how to get the correct port relayed to the emulator
  • What if the port and URI change? How does the app know
  • Distributed release app will need to talk to the web server, so the server needs to be up and running all the time somewhere.

None of these options seemed to be covering everything we needed so. We had this idea;

Put the web server into the app

This is not too different from any app with an embedded web server. This means to have a web server process spawned when the app launches, then serve mock endpoints on localhost:3169 (whatever port you want, really). Then your app talks to itself to fetch from the mock API.

This could, of course, be achieved in multiple ways also. For example, you could embed any web server via a native module, similarly in react-native.

This was particularly simple to do using flutter. In addition, using both the same process and the same language and codebase made it easy to make and extend.

  • Server restarts when you hot-restart the flutter app
  • What port will be serving is in your code, so your API client knows which port to listen from.
  • Re-use data models made for dart.
  • Same JSON encoding/decoding approach with the actual app code.

When you compare this with the other options above;

Pros

  • Server in the same app
  • Same language and project
  • Easy to develop
  • Lots of shared code

Cons

  • Maybe the port would clash with something (but you can change the port)

Implementation of this is based on flutter’s own HTTP server. So use a convenience wrapper around it with shelf package. And for making intercepting requests more declaratively similar to Express / Sinatra / Flask -ish way, used shelf_router package.

So here’s what the server code looks like;

1import 'dart:convert';
2
3import 'package:logging/logging.dart';
4import 'package:shelf/shelf.dart';
5import 'package:shelf/shelf_io.dart' as shelf_io;
6import 'package:shelf_router/shelf_router.dart' as shelf_router;
7
8Logger log = Logger('LocalMockApiServer');
9
10class LocalMockApiServer {
11 static final host = 'localhost';
12 static final port = 3131;
13 static get baseUrl => 'http://$host:$port/';
14
15 late shelf_router.Router app;
16
17 LocalMockApiServer() {
18 app = shelf_router.Router();
19
20 app.get('/user/account', (Request req) async {
21 return JsonMockResponse.ok({
22 'id': req.accessToken,
23 });
24 });
25
26 app.get('/user/investments', (Request req) async {
27 return JsonMockResponse.ok([
28 {
29 'key': 'value',
30 },
31 {
32 'key': 'value',
33 },
34 ], delay: 1200);
35 });
36 }
37
38 Future<void> start() async {
39 log.info('starting...');
40
41 var handler = const Pipeline().addMiddleware(
42 logRequests(logger: (message, isError) {
43 if (isError)
44 log.severe(message);
45 else
46 log.info(message);
47 }),
48 ).addHandler(app);
49
50 var server = await shelf_io.serve(handler, host, port);
51 server.autoCompress = true;
52
53 log.info('serving on: $baseUrl');
54 }
55}
56
57extension on Request {
58 Future<String?> bodyJsonValue(String param) async {
59 return jsonDecode(await this.readAsString())?[param];
60 }
61
62 String? get accessToken =>
63 this.headers['Authorization']?.split('Bearer ').last;
64}
65
66extension JsonMockResponse on Response {
67 static ok<T>(T json, {int delay = 800}) async {
68 await Future.delayed(Duration(milliseconds: delay)); // Emulate lag
69 return Response.ok(
70 jsonEncode(json),
71 headers: {'Content-Type': 'application/json'},
72 );
73 }
74}

Let’s break this down and see what is happening;

The gist is that we create a class called LocalMockApiServer, which sets up the server and has an async start method. I removed some implementation details from below for brevity. The complete class implementation is above.

1class LocalMockApiServer {
2 static final host = 'localhost';
3 static final port = 3131;
4 static get baseUrl => 'http://$host:$port/';
5
6 late shelf_router.Router app;
7
8 LocalMockApiServer() {
9 app = shelf_router.Router();
10
11 // 1: Request handler here
12 // 2: Request handler here
13 }
14
15 Future<void> start() async {
16 // 3: Log processor setup here
17
18 var server = await shelf_io.serve(handler, host, port);
19 server.autoCompress = true;
20
21 log.info('serving on: $baseUrl');
22 }
23}

Then we call the start method from our main.dart some place between WidgetsFlutterBinding.ensureInitialized() and runApp

1void main() async {
2 // ...
3 WidgetsFlutterBinding.ensureInitialized();
4 // ...
5 await LocalMockApiServer().start();
6
7 runApp(
8 //...
9 );
10}

Doing this will start the server in the background, and your app should launch as it should before. You should see the logs;

1LocalMockApiServer: starting...
2LocalMockApiServer: serving on: http://localhost:3131

awaiting on LocalMockApiServer().start() will ensure that server is up and running before your app starts calling the endpoints.

Implement your mock API endpoint handlers. These go where I marked // 1 and // 2 above;

Parsing access token from request headers and responding to it as user id (doesn’t make sense but just for testing and visibility)

1app.get('/user/account', (Request req) async {
2 return JsonMockResponse.ok({
3 'id': req.accessToken,
4 });
5});

JsonMockResponse.ok Request.accessToken are implemented as convenience extensions. I will explain them below.

Responding with an array as the root of the JSON;

1app.get('/inventory', (Request req) async {
2 return JsonMockResponse.ok([
3 {
4 'key': 'value',
5 },
6 {
7 'key': 'value',
8 },
9 ], delay: 1200); // Maybe you need more than the default delay
10});

If you want a post request handler, swap get with post after app. bodyJsonValue method on Request is implemented as an extension. Explained below after this block.

1app.post('/account/create', (Request req) async {
2 var token = await req.bodyJsonValue('token');
3 log.info('create account request for token: $token');
4 return JsonMockResponse.ok({
5 'id': 'mock_id_${token?.substring(0, 8)}',
6 });
7});

Set up the log processor. Below, this part replaces // 3 above inside the start method. This is for intercepting the server logs and plugging in whatever logging system you have. I’m using logger.

1Future<void> start() async {
2 // ...
3
4 var handler = const Pipeline().addMiddleware(
5 logRequests(logger: (message, isError) {
6 if (isError)
7 log.severe(message);
8 else
9 log.info(message);
10 }),
11 ).addHandler(app);
12
13 // ...
14}

And then we have a couple of quality-of-life extensions on top of flutter’s Request and Response. Include these after the LocalMockApiServer definition.

Request extension adds two things;

  • bodyJsonValue: Reads request body then decodes it as JSON.
  • accessToken: Reads standard OAuth Authorization header’s Bearer value.
1extension on Request {
2 Future<String?> bodyJsonValue(String param) async {
3 return jsonDecode(await this.readAsString())?[param];
4 }
5
6 String? get accessToken =>
7 this.headers['Authorization']?.split('Bearer ').last;
8}

JsonMockResponse is an extension based on Response with a re-definition of ok. Which responds with 200 status codes and encodes the given map into a JSON string as the body. Responds with an optionally configurable delay.

Since this code is likely to be repeated in every response, it saves time by not writing Content-Type headers and encoding code repeatedly.

1extension JsonMockResponse on Response {
2 static ok<T>(T json, {int delay = 800}) async {
3 await Future.delayed(Duration(milliseconds: delay)); // Emulate lag
4 return Response.ok(
5 jsonEncode(json),
6 headers: {'Content-Type': 'application/json'},
7 );
8 }
9}

This is it. Then call these endpoints as you would from any local server, as in http://localhost:3131/inventory. Don’t forget that this server runs on the phone or emulator/simulator. Unless you are targeting desktop or web. This server won’t be accessible from your desktop environment.

This solution is a simple idea that was surprisingly easy to implement and saved me a lot of time. I was able to implement the API Client code, data models, deserialization, and serialization even before any APIs were developed. It was handy as this mock server is distributed along with the app itself, from Google play internal testing or test flight, etc. Remember that all of these codes are meant to be deleted once the actual APIs exist. Let me know what you think about this idea!


I'm Eralp Karaduman, a software engineer. I make apps, games and digital toys. Visit eralpkaraduman.com to learn more about me, also find me on other internets: Twitter, GitHub, LinkedIn, RSS.