Calling REST APIs from Dart and Flutter
Tags: dart, flutter, rest
Published: 2021-10-02 10:27:29

---
title: Calling REST APIs from Dart and Flutter
published: true
description: Concept for a building a well-tested Dart SDK for a REST API for use in a Flutter project
tags: dart, flutter, rest
---

When using API-based services, they often bring an SDK for some of the most popular languages, to facilitate the creation of apps using their API (e.g. [Supabase](https://supabase.io/docs/reference/javascript/supabase-client) or [Google Ads](https://developers.google.com/google-ads/api/docs/client-libs)). This blog post describes a concept for building your own SDK for consumption of any REST API, using the Dart programming language for use in an app using the Flutter framework.

--------

**Table of contents**

- [Terminology](#terminology)
- [Context](#context)
- [Usage](#usage)
  * [Package dependency declaration](#package-dependency-declaration)
  * [Instantiate API entry point](#instantiate-api-entry-point)
  * [Collection resources](#collection-resources)
  * [Singleton resources](#singleton-resources)
  * [Error handling](#error-handling)
  * [Usage in Flutter project](#usage-in-flutter-project)
- [Testing](#testing)
  * [Service tests](#service-tests)
  * [Widget tests](#widget-tests)
- [Implementation details](#implementation-details)
  * [Overview](#overview)
  * [Payload representations](#payload-representations)
  * [Tests](#tests)

--------


## Terminology

The following table gives an overview of the abbreviations and technical terms used in the blog post.

| Term | Description |
|------|-------------|
| API | Application Programming Interface; an interface of a software system that allows other software systems to use it. |
| SDK | Software Development Kit; a library that provides access to a third-party service for the respective programming language. |
| REST API | Short form for RESTful API which denotes a software architecture style based "representational state transfer". |
| KSCH | Kirpal Sagar Charitable Hospital; the target domain of the app to be built. |
| Workflow | A semi-automated business process. |

## Context

The [backend](https://github.com/ksch-workflows/backend/) service of the KSCH Workflows system provides [a REST API](https://ksch-workflows.github.io/backend) for all the queries and commands which are needed for the apps in the KSCH workflows. The [KSCH Dart Client](https://github.com/ksch-workflows/ksch-dart-client) facilitates it for all apps which use the Dart programming language to use that API by wrapping the low-level REST API with a high-level Dart API. The apps - e.g. the *[Registration desk](https://github.com/ksch-workflows/registration-desk/)*, *Pharmacy desk*, and *Administration app* - can then use the *KSCH Dart Client* like any other Dart package.

![Context diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/spnon9800g0n900l2lom.png)

In the text below, the *KSCH Dart Client* will also be referred to as "the SDK".

## Usage

Before the discussion of the internal structure of the *KSCH Dart Client*, it will be shown how to use it.

### Package dependency declaration

The *KSCH Dart Client* is a project-specific library that is of no use for the general Dart community. That's why the package is not published to the Dart package repository but is leveraging Dart's capability to declare dependencies directly on GitHub repositories.

```yaml
dependencies:
  ksch_dart_client:
    git:
      url: https://github.com/ksch-workflows/ksch_dart_client
      ref: a85c7f0cb83087d13e207b1331bcf6f64676e995
```

Once the system is in production use, [semantic versioning](https://semver.org/) tags should be used instead of specific commit IDs.

### Instantiate API entry point

The next step in using the SDK is to create an instance of the `KschApi` class. This class has a parameter for the base URL of the backend service so that it can be configured to be executed against the testing or production system.

```dart
KschApi api = KschApi('http://localhost:8080');
```

### Collection resources

The first request to be done with the API is listing all patients with a GET request on the `/patients` collection resource.

```text
http GET /patients
```

The basic idea of the SDK is that it provides a property in the `api` object for every supported resource. So, in the case of this example, there is a `patients` property. On its value is a method available for each operation that can be done on that resource. By calling this method, the HTTP call is triggered and delivers its response wrapped in a `Future`.

```dart
await api.patients.list();
```

Since collection resources are usually paged, the `list` method can take the index of the page to be requested as an optional parameter.

```dart
late PatientsResponsePayload response;
var page = 0;
do {
  response = await api.patients.list(page: page++);
  for (var patient in response.patients) {
    print(patient.id);
  }
} while (response.hasNextPage);
```

### Singleton resources

For working with individual patients, the singleton resource can be used.

```text
http GET /patients/${PATIENT_ID}
```

With the help of Dart's [`call` method convention](https://www.educative.io/edpresso/what-is-dart-call) it is possible to invoke an object as a function. With this tool, a parameter can be passed to a collection resource for fluent access on a singleton resource below.

```dart
var response = await api.patients(patientId).get();
print(response.id);
print(response.name);
print(response.residentialAddress);
```

Each resource may also have sub-resources. All resource path elements act as a builder and the HTTP call is triggered only in the final method call.

```dart
await api.patients(patientId).visits.startVisit(VisitType.OPD);
```

### Error handling

When there is an error response from the API, i.e. a status code >= 400, then an exception will be raised.

### Usage in Flutter project

To avoid any HTTP-related ideas and too much business logic within the frontend widgets, the SDK is wrapped in a service layer. For every service, there is an interface which is widgets uses. During application start the `main.dart` file decides which service implementation should be used. Only that service implementation knows about the SDK and how to use it.

![Service structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gzhnz5pt0xqptnwa6ymh.png)

The SDK can now be used in any Dart project. To spare the Flutter code of dealing with the request details, the SDK can be wrapped into service classes.

```dart
class PatientServiceImpl implements PatientService {
  final KschApi _api;

  PatientServiceImpl(KschApi api) : _api = api;

  @override
  Future<Patient> get(String patientId) async {
    var response = await _api.patients(patientId).get();
    return Patient.from(response);
  }
}
```

The service can then be injected into the widget using e.g. [get_it](https://pub.dev/packages/get_it) package.

```dart
class _RegisterPatientPageState extends State<RegisterPatientPage> {
  final PatientService patientService = GetIt.I<PatientService>();

  @override
  Widget build(BuildContext context) {
    return WebScaffold(
      title: 'Register patient',
      // ...
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          // ...
          final RegisterPatientResult? result = await showDialog(
              //...
              );
          if (result != null) {
            final createdPatient = await patientService.create(result.patient);
            // ...
          }
        },
        // ...
      ),
    );
  }
}
```

## Testing

### Service tests

The service classes can be tested with the help of the [nock](https://pub.dev/packages/nock) package.

```dart
void main() {
  late PatientServiceImpl patientService;

  setUpAll(nock.init);

  setUp(() {
    nock.cleanAll();

    patientService = PatientServiceImpl('http://localhost');
  });

  test('Should create patient in case of emergency', () async {
    var patientId = const Uuid().v4();
    givenCreatePatientResponse(MinimalPatientResponse(patientId).toJson());

    var result = await patientService.create(null);

    expect(result.id, equals(patientId));
  });
}

void givenCreatePatientResponse(dynamic body) {
  nock('http://localhost').post('/api/patients')..reply(200, body);
}
```

### Widget tests

For the unit tests of the Flutter widgets, it should be fairly simple to create mocks for the used services.


## Implementation details

Next comes a description of how the SDK is working internally.

### Overview

The [KSCH Dart Client](https://github.com/ksch-workflows/ksch-dart-client) as a small core with the `KschApi` class and model classes to be able to interpret pagination and links in the response payloads. The `KschApi` API provides an API for basic HTTP operations which can then be used by the resources. Further, it provides properties to the root resources.

The resources themselves are a tree structure, i.e. every resource can have subresources. Further, they know about their respective path element. Just before the actual HTTP called on the `absolutePath`, all the path elements of all ancestors are joined together.

The bases classes for the collection resources and identity resources are almost the same, with the difference that 
identity resources know about their identification number.

![ksch-dart-client-uml](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lp2glf4vitcyw2pyqphc.png)

### Payload representations

The KSCH API uses JSON as the data format for request and response payloads. The code which does the mapping between JSON strings and Dart data types is generated with the help of the [json_annotation](https://pub.dev/packages/json_annotation) library.

The Dart data types are a model of the JSON data structures. By default, the JSON properties are mapped with the Dart properties. If this is not possible, e.g. because a property in the JSON data starts with an underscore, then the mapping can be configured with custom annotation.

```dart
@JsonSerializable()
class VisitResponsePayload {
  @JsonKey(name: '_id')
  final String id;

  final VisitType type;

  final String opdNumber;

  final DateTime timeStart;

  VisitResponsePayload({
    required this.id,
    required this.type,
    required this.opdNumber,
    required this.timeStart,
  });

  factory VisitResponsePayload.fromJson(Map<String, dynamic> json) =>
      _$VisitResponsePayloadFromJson(json);

  Map<String, dynamic> toJson() => _$VisitResponsePayloadToJson(this);
}
```

The details of the `fromJson` and `toJson` methods are then generated from the Dart build runner.

```
dart run build_runner build
```

### Tests

The unit tests for the SDK act also as API tests for the [backend](https://github.com/ksch-workflows/backend) API. The expectation is that the *backend* has been locally started on port 8080. Then they are using the real API to make sure that all requests can be successfully executed and all responses successfully parsed. Later on, this test suite will also be included in the build process of the *backend* to make sure that no unintended breaking API changes are made.