Working with APIs with Dart
Learning objectives
- You know how to use an API.
Application programming interfaces (APIs) provide endpoints for accessing and modifying data as well as for invoking operations on the server that hosts the API. In here, we mainly focus on APIs as data endpoints. Designing and building (APIs) are discussed to some extent e.g. in the Web Software Development course.
APIs and data format
When working with APIs, the format of the data provided by the API is defined by the people responsible for the API. These days, perhaps the most commonly used format is JSON, which we also use here. In practice, JSON is a string-based format, where the content is passed as a string with keys and values wrapped in curly brackets.
For example, the Dog API at https://dog.ceo/dog-api/ provides URLs for dog pictures. The API returns a JSON-formatted document with the fields message
and status
. A document returned by the Dog API can look, for example, as follows.
{
"message": "https://images.dog.ceo/breeds/shiba/shiba-9.jpg",
"status": "success"
}
Similarly, the Joke API at https://simple-joke-api.deno.dev/random returns a JSON-formatted document with the fields setup
and punchline
. A document returned by the Joke API can look, for example, as follows.
{
"setup": "What do you call a belt made out of watches?",
"punchline": "A waist of time."
}
APIs can also provide more complex data formats. As an example, the currency API at https://api.frankfurter.app/latest?from=EUR&to=USD provides the most recent rates for the given currencies (here, from EUR
to USD
). The API returns a JSON-formatted document with the fields amount
, base
, date
, and rates
. The field rates
contains key-value pairs -- as we did define a target currency, there is just one rate.
{
"amount":1.0,
"base":"EUR",
"date":"1999-12-30",
"rates":{
"USD":1.0046
}
}
Every JSON document shown above could also be represented as a class. As an example, a Joke
could be represented as follows.
class Joke {
String setup;
String punchline;
Joke(this.setup, this.punchline);
}
Programmatic API access
For programmatic API access, we rely on the http-package, which is used for retrieving information from a given address. The address is represented as an instance of Uri, which is created from a string using the method Uri.parse.
The instance of Uri
is passed to the asynchronous get function of the http
-package, which returns a Response as a result of an asynchronous operation, i.e. a Future
.
The Response
has a string-property body
, which contains the body of the response. Retrieving and printing a joke from the Joke API is done as follows.
Parsing a JSON string
When the JSON document is retrieved, it is formatted as a string in the body of the response. To parse it into a more useful format, we use the jsonDecode function of the dart:convert
package.
In the following example, instead of printing the response body directly, we decode the body before printing.
In practice, the jsonDecode
function determines the value that the function returns based on the JSON document format. For a document consisting of key-value pairs, such as the examples above, the function returns a map.
The following example demonstrates the use of the jsonDecode
function in an asynchronous function, where we wait for the completion of the get
request, and then parse the response to a map. Finally, the Joke stored in the map is printed.
Cross-Origin Resource Sharing
Cross-Origin Resource Sharing policies is a way to restrict browsers from accessing APIs. When running Dart and Flutter applications within these materials, the applications are compiled into JavaScript and run within the browsers. Accessing APIs that do not allow Cross-Origin Resouce Sharing does not work from within a browser -- for mobile applications, desktop applications, etc that do not run within a browser, this restriction is lifted.
You'll learn a bit more about this in the Web Software Development course.
API encapsulation
In practice, we would encapsulate APIs to separate classes that would be used to access them. For the Joke API, we would encapsulate the API into a class called, for example, JokeService
, which would provide a method for retrieving random jokes. Jokes would also be represented using a class, which would provide us a vocabulary to work with.
In the following example, we have a class JokeService
that provides a method getRandomJoke
. Further, we also have a Joke
class that represents a joke.
Now, from the point of view of the API user -- the main
method, the usage of the API is straightforward.
Posting data to an API
In the previous example, we used an API to retrieve data. Naturally, we can also post data to an API. Posting data is done using the post function of the http-package. The function post
is given an instance of a Uri
, possible request headers, and the body of the request. The example below shows the key parts of a post
request.
var address = 'https://api.address';
var headers = {'Content-Type': 'application/json; charset=UTF-8'};
var content = {'key': 'value'};
await http.post(
Uri.parse(address),
headers: headers,
body: jsonEncode(body)
);
The function jsonEncode is used to transform a given map to a string-formatted JSON-document.
Let's look at this using the Sum API at https://fitech-api.deno.dev/sum-api. The API accepts JSON-formatted data where the data must contain the fields one
and two
, which have numeric values. The API returns a JSON-formatted document with the field sum
that contains the sum of the sent values.
For example, if the JSON-document sent to the server contains the values 1
and 2
for the fields one
and two
.
{
"one": 1,
"two": 2
}
The response will have a field sum
with the value 3
.
{
"sum": 3
}
The API would be used as follows.