Simple Dart REST API

Introduction

Today we will look at how to create a simple REST API using dart. How to start the project and how to make different endpoints and make some integration tests. What do you need for this tutorial:

  • Dart CLI
  • nodemon will be useful for reloading the server every time you save something

Create the project

Let's create the project with this simple command:

dart create -t server-shelf api_rest_dart

-t is for allowing us a template, dart has some default templates like:

  • console-simple: A simple command-line application. (default)
  • console-full: A command-line application sample.
  • package-simple: A starting point for Dart libraries or applications.
  • server-shelf: A server app using package:shelf
  • web-simple: A web app that uses only core Dart libraries.

We can find more information with the command:

dart create --help

Here we will use server-shelf to create the project. Take a look at what dart just create for us:

➜  api_rest_dart ls -l
total 64
-rw-r--r--  1 etiennetheodore  staff    29 Oct  8 19:43 CHANGELOG.md
-rw-r--r--  1 etiennetheodore  staff   555 Oct  8 19:43 Dockerfile
-rw-r--r--  1 etiennetheodore  staff  1145 Oct  8 19:43 README.md
-rw-r--r--  1 etiennetheodore  staff  1038 Oct  8 19:43 analysis_options.yaml
drwxr-xr-x  3 etiennetheodore  staff    96 Oct  8 19:43 bin
-rw-r--r--  1 etiennetheodore  staff  8410 Oct  8 19:44 pubspec.lock
-rw-r--r--  1 etiennetheodore  staff   342 Oct  8 19:43 pubspec.yaml
drwxr-xr-x  3 etiennetheodore  staff    96 Oct  8 19:43 test

If we check the bin/ folder you will find the default server.dart created by the command:

import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';

// Configure routes.
final _router = Router()
  ..get('/', _rootHandler)
  ..get('/echo/<message>', _echoHandler);

Response _rootHandler(Request req) {
  return Response.ok('Hello, World!\n');
}

Response _echoHandler(Request request) {
  final message = params(request, 'message');
  return Response.ok('$message\n');
}

void main(List<String> args) async {
  // Use any available host or container IP (usually `0.0.0.0`).
  final ip = InternetAddress.anyIPv4;

  // Configure a pipeline that logs requests.
  final _handler = Pipeline().addMiddleware(logRequests()).addHandler(_router);

  // For running in containers, we respect the PORT environment variable.
  final port = int.parse(Platform.environment['PORT'] ?? '8080');
  final server = await serve(_handler, ip, port);
  print('Server listening on port ${server.port}');
}

And the test file related to it in the test/ folder server_test.dart:

import 'package:http/http.dart';
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';

void main() {
  final port = '8080';
  final host = 'http://0.0.0.0:$port';

  setUp(() async {
    await TestProcess.start(
      'dart',
      ['run', 'bin/server.dart'],
      environment: {'PORT': port},
    );
  });

  test('Root', () async {
    final response = await get(Uri.parse(host + '/'));
    expect(response.statusCode, 200);
    expect(response.body, 'Hello, World!\n');
  });

  test('Echo', () async {
    final response = await get(Uri.parse(host + '/echo/hello'));
    expect(response.statusCode, 200);
    expect(response.body, 'hello\n');
  });
  test('404', () async {
    final response = await get(Uri.parse(host + '/foobar'));
    expect(response.statusCode, 404);
  });
}

All is in place we can now run the server.

Don’t miss out on the latest articles. Only one email by week about Flutter/Dart development.

your@email.com
Subscribe

Running the server in local

To run the server just use this:

nodemon -x "dart run bin/server.dart " -e dart

The nodemon is for reloading the server every time we make a change on the file.

Take a look in your favorite browser this URL: http://0.0.0.0:8080/

You should see a simple Hello, World! in your page. We can also use cURL to take a look:

➜  ~ curl http://0.0.0.0:8080/
Hello, World!

Your server is running great!

Create POST request

Now let's create an example endpoint to POST some data to the server.

We will first create a model Article to communicate data with the server based on this JSON:

{
   "title": "My super title",
   "content": "My super contact"
}
article.json

Then our model based on this JSON:

import 'dart:convert';

Article articleFromJson(String str) => Article.fromJson(json.decode(str));

String articleToJson(Article data) => json.encode(data.toJson());

List<Article> articlesFromJson(String str) => List<Article>.from(json.decode(str).map((x) => Article.fromJson(x)));

String articlesToJson(List<Article> data) => json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

class Article {
    Article({
        required this.title,
        required this.content,
    });

    final String title;
    final String content;

    factory Article.fromJson(Map<String, dynamic> json) => Article(
        title: json['title'],
        content: json['content'],
    );

    Map<String, dynamic> toJson() => {
        'title': title,
        'content': content,
    };
}
article.dart

Like you can see we have two methods to serialize our Article toJson and fromJson and for a List of Articles too. It's we will be helpful when we will create the endpoints.

Now we will add the route inside the router:

...
final _router = Router()
  ..get('/', _rootHandler)
  ..post('/articles', _postArticlesHandler);
...
server.dart

And we will create our handler:

List<Article> articles = [];


Future<Response> _postArticleHandler(Request request) async{
  String body = await request.readAsString();
  Article article = articleFromJson(body);
  articles.add(article);
  return Response.ok(articleToJson(article));
}
server.dart

Like we can see, to save an article we use List<Article> articles , of course in production we will use a database like MongoDB or others. Because here every time we re-start the server articles will be reset.

Now we can test if our new endpoint works:

➜  ~ curl -X POST http://0.0.0.0:8080/articles -H 'Content-Type: application/json' -d '{"title": "My super article","content": "My super content"}'
{"title":"My super article","content":"My super content"}%

Great now we have added an article to our list, but we want to see all articles, let's see how to do it in the next chapter.

Don’t miss out on the latest articles. Only one email by week about Flutter/Dart development.

your@email.com
Subscribe

Create GET articles

Same as the POST we will create another route:

final _router = Router()
  ..get('/', _rootHandler)
  ..get('/articles', _getArticlesHandler)
  ..post('/articles', _postArticleHandler);
server.dart

And the handler associated:

Response _getArticlesHandler(Request request) {
  return Response.ok(articlesToJson(articles));
}
server.json

Here we use the articlesToJson created before on the model Article, to return the list of articles. Again prefer to use a database instead of this (it's just for the example).

Before we pass to the next chapter take a look of the server.dart with these two new endpoints:

import 'dart:io';

import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart';
import 'package:shelf_router/shelf_router.dart';

import 'article.dart';

List<Article> articles = [];

// Configure routes.
final _router = Router()
  ..get('/', _rootHandler)
  ..get('/articles', _getArticlesHandler)
  ..post('/articles', _postArticleHandler);

Response _rootHandler(Request req) {
  return Response.ok('Hello, World!\n');
}

Future<Response> _postArticleHandler(Request request) async{
  String body = await request.readAsString();
  Article article = articleFromJson(body);
  articles.add(article);
  return Response.ok(articleToJson(article));
}

Response _getArticlesHandler(Request request) {
  return Response.ok(articlesToJson(articles));
}

void main(List<String> args) async {
  // Use any available host or container IP (usually `0.0.0.0`).
  final ip = InternetAddress.anyIPv4;

  // Configure a pipeline that logs requests.
  final _handler = Pipeline().addMiddleware(logRequests()).addHandler(_router);

  // For running in containers, we respect the PORT environment variable.
  final port = int.parse(Platform.environment['PORT'] ?? '8080');
  final server = await serve(_handler, ip, port);
  print('Server listening on port ${server.port}');
}

Don’t miss out on the latest articles. Only one email by week about Flutter/Dart development.

your@email.com
Subscribe

Integration test

Remove the useless test

Now let's talk about integration tests, when you have created your project a server_test.dart was created too, take a look:

import 'package:http/http.dart';
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';

void main() {
  final port = '8080';
  final host = 'http://0.0.0.0:$port';

  setUp(() async {
    await TestProcess.start(
      'dart',
      ['run', 'bin/server.dart'],
      environment: {'PORT': port},
    );
  });

  test('Root', () async {
    final response = await get(Uri.parse(host + '/'));
    expect(response.statusCode, 200);
    expect(response.body, 'Hello, World!\n');
  });

  test('Echo', () async {
    final response = await get(Uri.parse(host + '/echo/hello'));
    expect(response.statusCode, 200);
    expect(response.body, 'hello\n');
  });
  test('404', () async {
    final response = await get(Uri.parse(host + '/foobar'));
    expect(response.statusCode, 404);
  });
}

And if you run the test like so it should fail because we have removed some endpoints:

➜  api_rest_dart dart test
00:01 +1 -1: test/server_test.dart: Echo [E]
  Expected: <200>
    Actual: <404>

  package:test_api            expect
  test/server_test.dart 25:5  main.<fn>

00:01 +1 -1: loading test/server_test.dart
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.
00:01 +1 -1: test/server_test.dart: Echo

Process `dart run bin/server.dart` was killed with SIGKILL in a tear-down. Output:
00:02 +2 -1: Some tests failed.
➜  api_rest_dart

It's normal because we have removed the /echo/message endpoint. Let's fix the tests and remove the Echo test:

import 'package:http/http.dart';
import 'package:test/test.dart';
import 'package:test_process/test_process.dart';

void main() {
  final port = '8080';
  final host = 'http://0.0.0.0:$port';

  setUp(() async {
    await TestProcess.start(
      'dart',
      ['run', 'bin/server.dart'],
      environment: {'PORT': port},
    );
  });

  test('Root', () async {
    final response = await get(Uri.parse(host + '/'));
    expect(response.statusCode, 200);
    expect(response.body, 'Hello, World!\n');
  });

  test('404', () async {
    final response = await get(Uri.parse(host + '/foobar'));
    expect(response.statusCode, 404);
  });
}

And run the test:

➜  api_rest_dart dart test
00:02 +2: All tests passed!
➜  api_rest_dart

Adding new tests

We will now create the unit test for our POST endpoint like so:

test('POST articles', () async {
    final response = await post(
      Uri.parse(host + '/articles'),
      body: '{"title":"My super article","content":"My super content"}',
    );
    expect(response.statusCode, 200);
    expect(response.body, '{"title":"My super article","content":"My super content"}');
});

And when you pass a wrong body like an empty string, we can make a test for it too:

  test('POST articles with empty string', () async {
    final response = await post(
      Uri.parse(host + '/articles'),
      body: '',
    );
    expect(response.statusCode, 400);
  });

Run the test:

➜  api_rest_dart dart test
00:02 +2 -1: test/server_test.dart: POST articles with empty string [E]
  Expected: <400>
    Actual: <500>

  package:test_api            expect
  test/server_test.dart 43:5  main.<fn>

And we can change the code source to pass the test:

Future<Response> _postArticleHandler(Request request) async {
  String body = await request.readAsString();

  try {
    Article article = articleFromJson(body);
    articles.add(article);
    return Response.ok(articleToJson(article));
  } catch (e) {
    return Response(400);
  }
}
➜  api_rest_dart dart test
00:04 +4: All tests passed!

For the GET articles now:

test('GET articles', () async {
    final response = await get(
      Uri.parse(host + '/articles'),
    );
    expect(response.statusCode, 200);
    expect(response.body, '[{"title":"My super article","content":"My super content"}]');
});

A simple test to ensure the POST was a success and save an article in the list. And if we run the test it should pass:

➜  api_rest_dart dart test
00:05 +5: All tests passed!

Conclusion

We have seen how to create a simple API with Dart during this tutorial and some integration tests too. If you want to do more, you can try to use MongoDB in local to save the articles instead of the variable list. You can also deploy the API on Heroku for example or another server. When you create a project a Dockerfile is created too. And it allows you to deploy easier.

I hope you enjoy this simple tutorial, feel free to contact me on Twitter if you have any questions/suggestions.

Like always you can check the source code on GitHub:

GitHub - Kiruel/api_rest_dart
Contribute to Kiruel/api_rest_dart development by creating an account on GitHub.

Don’t miss out on the latest articles. Only one email by week about Flutter/Dart development.

your@email.com
Subscribe

Subscribe to Etienne Théodore

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe