Part 2: A Complete Guide For Building RESTful Applications Using Aqueduct
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:
- Dart (Install Instructions)
- IntelliJ IDEA or any other Jetbrains IDE, including the free Community Edition (Install Instructions)
- 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:
GET /heroes
to get the list of heroesGET /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:
- Part 1: A Complete Guide For Building RESTful Applications Using Aqueduct (Core Concepts)
- Part 2: A Complete Guide For Building RESTful Applications Using Aqueduct (Application Development)
- Part 3: A Complete Guide For Building RESTful Applications Using Aqueduct (DB Configuration, coming soon)
Useful resources
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.