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:
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.
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:
Then our model based on this JSON:
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:
And we will create our handler:
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.
Create GET articles
Same as the POST we will create another route:
And the handler associated:
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.
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:
Don’t miss out on the latest articles. Only one email by week about Flutter/Dart development.