What is a Router?

Routers are responsible for capturing the request path and determining the logic that runs based on it. The request path is defined by registering a route when calling the route method on a Router object that is supplied to us. The registration occurs when the setupRouter method in our FaveReadsSink subclass is called:

// lib/fave_reads_sink.dart @override

void setupRouter(Router router) {...}

The setupRouter method provides a Router object as a parameter, which we then use to define each of our routes:

@override

void setupRouter(Router router) {

router.route(’/path-1’).listen(...);

router.route(’/path-2’).listen(...);

router.route(’/path-3’).listen(...); // and so on

}

Calling the route method accepts a string containing the path name, followed by a listen method that then allows us to define the logic to run when a request is made to this path. The route can contain path variables, which are placeholder tokens that represent whatever value is in that segment of the path:

router.route('/items/:itemID');

The example above declares a path variable itemID , which matches “/items/0”, “/items/1”, “/items/foo” and so on. The value of itemID will be “0”, “1” and “foo” respectively.

Path variables can also be optional, so that allows us to set them like so:

router.route('/items/[:itemID]);

This means that route-matching will also include “/items” as a path name.

The documentation on Routers is quite comprehensive and I’d recommend checking that out.

So, how do I apply this?

With this knowledge let’s open up lib/fave_reads_sink.dart from the project and amend setupRouter implementation as follows:

The root path(/) now returns HTML content. We also have a “/books” route defined that accepts an optional path variable named index . This will be the endpoint for our CRUD operations.

Invoking the route method returns a RouteController which exposes the listen method for us to define our logic to run in. There are two other methods we can use, namely pipe and generate . The latter allows us to create a new HTTPController object that gives better handling of our request (more on that later).

The listen method accepts a closure containing a Request object that represents an incoming request. We can then pull the information we need from that, perform transformations, and return a response.

The current logic for “/books” return the same response regardless of the request action. Let’s modify this to return a different response for each of our actions:

This works as expected by the code quality gets pretty ugly quickly. It’s repetitive and makes an easy mess of things:

via Giphy

This can be cleaned up using an HTTPController !

What is an HTTPController?

HTTPControllers respond to HTTP requests by mapping them to a particular ‘handler method’ to generate a response. A request is sent to an HTTPController by a Router as long as its path is matched.

To create one we will create a BooksController that extends HTTPController in order to add the behaviour we want. Let’s do this now. Create controller/books_controller.dart in the lib directory with the below content:

Here’s a summation of what’s happening:

The BooksController subclass consists of 5 handler methods, known as responder methods. Each responder method is annotated with a constant reflecting the appropriate request method: @httpGet , @httpPost , @httpPut , @httpDelete . Other methods will use HTTPMethod , like @HTTPMethod('PATCH') . Each responder method returns a Future of type Response . Futures are to Dart what Promises are to JavaScript. A responder method can bind values from the request to its arguments. We see this with the getBook() responder method argument: @HTTPPath("index") int idx . Its path variable is cast to an integer and assigned to a variable named idx .

If none of the responder methods match the request method(e.g. PATCH), a 405 Method Not Allowed response is returned.

Let’s go to lib/fave_reads_sink.dart and use this controller:

import 'fave_reads.dart';

import 'controller/books_controller.dart';

...

...

@override

void setupRouter((Router router) async {

router

.route(‘/books/[:index]’)

.generate(() => new BooksController()); // replaces `listen` method

...

and run our project by doing aqueduct serve or dart bin/main.dart in the terminal.

We can test our responses by using Postman.

Testing a POST Request with Postman

Mocking our datasource

For our datasource, let’s create an array inside controller/books_controller.dart :

We will then update our responder methods inside BooksController to manipulate this dataset:

Learn more about Dart’s various array/list methods.

Restart the server again and test this out with Postman.

Please note: This is running in an isolate, which means that any side-effects can only be seen in Postman’s(or whatever tool) session. Opening a separate session(like the browser) will not show these changes. This is because isolates by design do not share state. Not to worry though–this will be resolved when we implement the real database.

Refactoring the solution

I should have finished already, but that will create more work for us in Part 3. I don’t want that to happen, therefore please bear with me on this final stretch 😊

So remember when I said you could bind values from the request to the arguments of a responder method? Well, we can refactor our POST operation to convert its payload to a map through the @HTTPBody() metadata:

@httpPost

Future<Response> addSingle(@HTTPBody() Map book) async {

books.add(book);

return new Response.ok('Added new book.');

}

Here, an attempt is made to parse the request payload as a Map type. We could also specify a custom type, not just using the inbuilt ones, as long as the custom type extends an HTTPSerializable type. Let’s do this by introducing a Book model inside lib/model/book.dart :

Here’s what is happening in summary:

Our Book model implements HTTPSerializable , which is a utility used to parse information from an HTTP request Defining asMap and readFromMap(Map requestBody) methods. The first will be used when a JSON response is being sent back to the client, while the latter retrieves the request body and extracts the data for populating our model’s properties.

Now we just need to use this model:

Restart the server and test the results with Postman.

Conclusion

We’ve made some significant progress by fleshing out the skeleton of our web APIs. I hope that this journey has been a fun challenge so far. I’d encourage you to go through the further reading materials below to grasp the concepts we’ve covered.

As always I’m open to receiving feedback. Let me know what you liked about this tutorial, what you disliked and what you would like to see in future. I’d really be grateful for that.

And this concludes Part 2 of the series. The source code is available on github and Part 3 is now available. Like and follow me if you enjoyed this article for more content on Dart.

Further reading