Part 2: A Complete Guide For Building RESTful Applications Using Aqueduct

Zubair Rehman
ITNEXT
Published in
7 min readSep 5, 2019

--

source: https://bs-uploads.toptal.io

This article is the continuation of my first article on A Complete Guide For Building RESTful Applications Using Aqueduct. In the previous article we learned about aqueduct and its core concept. In this article we will learn how to configure and build our very first application using Aqueduct. By the end of this article, you will have created an Aqueduct application that serves fictional heroes.

Before we move on, if you’re unfamiliar with Aqueduct, i would encourage you to read my first article where i have given a brief introduction to Aqueduct and its core concepts.

Introduction

Aqueduct is a modern Dart HTTP server framework. The framework is composed of libraries for handling and routing HTTP requests, object-relational mapping (ORM), authentication and authorization (OAuth 2.0 provider) and documentation (OpenAPI). These libraries are used to build scalable REST APIs that run on the Dart VM.

Getting Started

To get started, make sure you have the following software installed:

  1. Dart (Install Instructions)
  2. IntelliJ IDEA or any other Jetbrains IDE, including the free Community Edition (Install Instructions)
  3. The IntelliJ IDEA Dart Plugin (Install Instructions)

Activating Aqueduct

Activate Aqueduct by running pub global activate aqueduct on your terminal. This will download and install all the necessary dependencies required to work with Aqueduct.

Creating a Project

Create a new project by running aqueduct create heroes on your terminal. This will create a heroes project under the directory specified in your terminal. You can open the project directory in IntelliJ IDE, Atom or Visual Studio Code. All three IDEs have a Dart plugin available.

In the project view, locate the lib directory; this is where your project's code will go. This project has two source files - heroes.dart and channel.dart. Open the file heroes.dart. Click Enable Dart Support in the top right corner of the editor.

Note: A project name must be snake_case

There are other templates exist that contain foundational code for using Aqueduct’s ORM and OAuth 2.0 implementation. These templates can be listed:

aqueduct create list-templates

You may provide the name of a template when creating a project to use that template:

aqueduct create -t db [my_project_name]

Running a Project

Now that we have successfully created a project, its time to run our application by simply running aqueduct serve from our project directory. For running within an IDE, run bin/main.dart. By default, a configuration file named config.yaml will be used.

Enter the following URL in your browser (I am using port 8888. This port might be different from yours and is visible in the terminal after starting aqueduct server)

http://localhost:8888/example

You should be able to see the following output in your browser

{"key":"value"}

Lets add some more code to our heroes project to get a list of heroes, and to get a single hero by its identifier. Those requests are:

  1. GET /heroes to get the list of heroes
  2. GET /heroes/:id to get an individual hero

But before we dive into implementation, lets have a quick look at how our requests are gonna handle.

Controller Objects Handle Requests

Requests are handled by controller objects. A controller object can respond to a request. It can also take other action and let another controller respond.

Our application will link two controllers:

  • a Router that makes sure the request path is /heroes or /heroes/:id
  • a HeroesControllers that responds with hero objects

Your application starts with a channel object called the application channel. You link the controllers in your application to this channel. Each application has a subclass of ApplicationChannel that you override methods in to set up your controllers. This type is already declared in lib/channel.dart - open this file and find ApplicationChannel.entryPoint:

@override
Controller get entryPoint {
final router = Router();

router
.route('/example')
.linkFunction((request) async {
return Response.ok({'key': 'value'});
});

return router;
}

When your application gets a request, the entryPoint controller is the first to handle it. In our case, this is a Router - a subclass of Controller. We need to route the path /heroes to a controller of our own, so we can control what happens.

Let's create a HeroesController by creating a new file in lib/controller/heroes_controller.dart and add the following code (you will need to create the subdirectory lib/controller/):

import 'package:aqueduct/aqueduct.dart';
import 'package:heroes/heroes.dart';

class HeroesController extends Controller {
final _heroes = [
{'id': 11, 'name': 'Captain America'},
{'id': 12, 'name': 'Ironman'},
{'id': 13, 'name': 'Wonder Woman'},
{'id': 14, 'name': 'Hulk'},
{'id': 15, 'name': 'Black Widow'},
];
@override
Future<RequestOrResponse> handle(Request request) async {
return Response.ok(_heroes);
}
}

Notice that HeroesController is a subclass of Controller it overrides handle method by returning a 200 OK status code, and a body containing a JSON-encoded list of heroes Response object.

Right now, our HeroesController isn't hooked up to the application channel. We need to link it to the router. First, import our new file at the top of channel.dart.

import 'controller/heroes_controller.dart';

Then link this HeroesController to the Router for the path /heroes:

@override
Controller get entryPoint {
final router = Router();
router
.route('/heroes')
.link(() => HeroesController());
router
.route('/example')
.linkFunction((request) async {
return Response.ok({'key': 'value'});
});
return router;
}

We now have an application that will return a list of heroes. Let’s stop our application using Ctrl+ c and restart by running the following command from the command-line:

aqueduct serve

Enter the following URL in your browser

http://localhost:8888/heroes

You should be able to see the following output in your browser

[
{"id":11,"name":"Captain America"},
{"id":12,"name":"Ironman"},
{"id":13,"name":"Wonder Woman"},
{"id":14,"name":"Hulk"},
{"id":15,"name":"Black Widow"}
]

Congratulations, you are done setting up the first part which is getting a list of heroes. Now, let’s move on to the next part of retrieving a single hero by providing its :id.

Advanced Routing

Right now, our application handles GET /heroes requests. Now we would like to send a request to retrieve a single hero. Let’s modify the /heroes route in channel.dart and add the following code:

router
.route('/heroes/[:id]')
.link(() => HeroesController());

Anything within the square brackets is optional, in our case its the :id portion of our route. Let’s modify heroes_controller.dart to add support for handling single hero request.

@override
Future<RequestOrResponse> handle(Request request) async {
if (request.path.variables.containsKey('id')) {
final id = int.parse(request.path.variables['id']);
final hero = _heroes.firstWhere((hero) => hero['id'] == id, orElse: () => null);
if (hero == null) {
return Response.notFound();
}
return Response.ok(hero);
}
return Response.ok(_heroes);
}

Let’s restart our application and make single hero request by typing in the following URL in our browser:

http://localhost:8888/heroes/11

The output will be like this:

{"id":11,"name":"Captain America"}

Note: You can also use the terminal to do request, for example: curl -X GET http://localhost:8888/heroes/11. You can also trigger a 404 Not Found response by getting a hero that doesn’t exist.

Our HeroesController is OK right now, but it'll soon run into a problem: what happens when we want to create a new hero? Or update an existing hero's name? Our handle method will start to get unmanageable, quickly.

That’s where ResourceController comes in.

ResourceControllers and Operation Methods

A ResourceController allows you to create a distinct method for each operation that we can perform on our heroes.

In heroes_controller.dart, replace HeroesController with the following:

class HeroesController extends ResourceController {
final _heroes = [
{'id': 11, 'name': 'Captain America'},
{'id': 12, 'name': 'Ironman'},
{'id': 13, 'name': 'Wonder Woman'},
{'id': 14, 'name': 'Hulk'},
{'id': 15, 'name': 'Black Widow'},
];
@Operation.get()
Future<Response> getAllHeroes() async {
return Response.ok(_heroes);
}
@Operation.get('id')
Future<Response> getHeroByID() async {
final id = int.parse(request.path.variables['id']);
final hero = _heroes.firstWhere((hero) => hero['id'] == id, orElse: () => null);
if (hero == null) {
return Response.notFound();
}
return Response.ok(hero);
}
}

Notice that we didn’t have to override handle in ResourceController. A ResourceController implements this method to call one of our operation methods. An operation method - like getAllHeroes and getHeroByID - must have an Operation annotation. The named constructor Operation.get means these methods get called when the request's method is GET. An operation method must also return a Future<Response>.

getHeroByID's annotation also has an argument - the name of our path variable :id. If that path variable exists in the request's path, getHeroByID will be called. If it doesn't exist, getAllHeroes will be called.

Request Binding

An operation method can declare parameters and bind them to properties of the request. When our operation method gets called, it will be passed values from the request as arguments. Request bindings automatically parse values into the type of the parameter (and return a better error response if parsing fails). Change the method getHeroByID():

@Operation.get('id')
Future<Response> getHeroByID(@Bind.path('id') int id) async {
final hero = _heroes.firstWhere((hero) => hero['id'] == id, orElse: () => null);
if (hero == null) {
return Response.notFound();
}
return Response.ok(hero);
}

Congratulations, we have created an Aqueduct application that serves fictional heroes. Till this point we are hard coding arrays into our code. What we would like to do next is to store heroes into the database and perform CRUD operations.

Thank you for reading! Feel free to say hi or share your thoughts on LinkedIn @zubairehman or in the responses below!

Next articles

The story does not finish yet, in the next article we learn how to configure and perform CRUD operations using PostgreSQL, so stay tuned :)

Other parts of this post:

Useful resources

http://aqueduct.io/docs/

Thats it for this article, if you liked this article, don’t forget to clap your hands 👏 as many times as you can to show your support, leave your comments and share it with your friends.

--

--