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

Zubair Rehman
ITNEXT
Published in
6 min readSep 5, 2019

--

source: https://stablekernel.com/wp-content/uploads/2017/07/aqueduct-1500x660.png

This article is the first in the series of articles about building a RESTful application in Dart using Aqueduct.

If you’re unfamiliar with the Aqueduct and its core concepts, a brief introduction to these notions are given below. Feel free to skip ahead to the next part if you’re already familiar with them (even though a quick refresh won’t hurt).

What is Aqueduct?

Aqueduct is an HTTP web server framework for building REST applications written in Dart. REST is short for Representational State Transfer and allows client-server interaction using HTTP protocol. Each url is called request while data sent back is call response.

A RESTful API is an application program interface (API) that uses HTTP requests to GET, PUT, POST and DELETE data.

Core Concepts

Before we move on to the implementation, lets quickly go-through the core concepts we will be using to build a RESTful application.

Resources

Resources are the things your application exposes through its HTTP API. For example, the GitHub API exposes organization, repository, issues and pull request resources; a social network API has profiles, posts, and user relationships. Resources are organized into collections (e.g., all of the posts), for which individual resources within that collection can be uniquely identified (e.g., a single post).

Routing

An application exposes routes for each resource it manages. A route is a string that matches the path of a request. Resources are identified by the path of an HTTP request. For example, the URL http://example.com/organizations identifies the collection of organization resources on the server http://example.com. The route /organizations/:id identifies a single organization and will match the paths /organizations/1, /organizations/2, and so on.

// Matches /organizations and /organizations/:id
route("/organizations/[:id]")...;

// Matches /organizations/:organizationId/posts
// and /organizations/:organizationsId/posts/:postId
route("/organizations/:organizationsId/posts/[:postId]")...;

// Matches /organizations/:organizationsId/posts
// and /organizations/:organizationsId/notes/:noteId
route("/organizations/:organizationsId/notes/[:noteId]")...;

Controllers

Controllers are objects that handle requests. For example, a controller might fetch rows from a database and send them to the client in the response body. Another controller might verify the username and password of a request. For example, a controller could return a 200 OK response with a JSON-encoded list of organization. A controller could also check a request to make sure it had the right credentials in its authorization header.

There are two flavors of controllers. An endpoint controller and a middleware controller.

  • Endpoint Controller

An endpoint controller performs operations on a resource or resource collection, and always sends a response. Endpoint controller fulfils requests by returning the state of a resource or by changing the state of a resource. You write most of your application-specific logic using endpoint controllers.

  • Middleware Controller

A middleware controller takes an action for a request, but isn’t responsible for fulfilling the request. Middleware controllers can do many different things and are often reusable in many channels. Most often, a middleware controller validates something about a request before it reaches an endpoint controller or to send a response for a request, to prevent any other controller in that channel from handling the request.

A channel must have exactly one endpoint controller. It can be preceded by zero or more middleware controllers. See the guides on Controllers and ResourceControllers for usage details.

class Authorizer extends Controller {
@override
Future<RequestOrResponse> handle(Request request) async {
if (isValid(request)) {
return request;
}

return Response.unauthorized();
}
}

Channels

Controllers are linked together to form a series of actions to take for a request. These linked together controllers are called a channel. For example, a channel might verify the credentials of a request and then return a list of organizations by composing two controllers that take each of these actions.

The Application Channel

The application channel is an object that contains all of the controllers in an application. It designates one controller as the first controller to receive every request called its entry point. Controllers are linked to the entry point (directly or transitively) to form the entire application channel. In nearly every application, the entry point is a router; this controller splits the channel into sub-channels for a given route.

The application channel is also responsible for initializing the application’s services, reading configuration files and other startup related tasks.

See the guide on the Application Channel for more details.

class AppChannel extends ApplicationChannel {
@override
Controller get entryPoint {
final router = new Router();

router
.route("/users")
.link(() => Authorizer())
.link(() => UserController());

return router;
}
}

Services

A service is an object that encapsulates complex tasks or algorithms, external communication or tasks that will be reused across an application. The primary users of service objects are controllers. Controllers often need to get (or create) information from outside the application. The most common example is database access, another REST API, a connected device, etc.

A service object encapsulates the information and behaviour needed to work with an external system and injects into controller by passing them as arguments to the controller’s constructor. The controller keeps a reference to the service, so that it can use it when handling a request.

For more details on injecting services, see the guide on the Application Channel.

class AppChannel extends ApplicationChannel {
PostgreSQLConnection database;

@override
Future prepare() async {
database = PostgreSQLConnection();
}

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

router
.route("/users")
.link(() => new Authorizer())
.link(() => new UserController(database));

return router;
}
}

Isolates

Isolates are memory-isolated threads; an object created on one isolate can’t be referenced by another isolate. When an application starts, one or more isolates containing replicas of your application code are spawned. This behaviour effectively ‘load balances’ your application across multiple threads.

A benefit to this structure is that each isolate has its own set of services, like database connections. This eliminates the need for techniques like ‘database connection pooling’, because the entire application is effectively ‘pooled’.

Bindings

A request might contain headers, query parameters, a body and path parameters that need to be parsed, validated and used in controller code. Bindings are annotations added to variables that perform this parsing and validation automatically. Appropriate error responses are sent when a bound value can’t be parsed into expected type or validation fails.

// Path Variable
@Bind.path(pathVariableName)
// URL Query Parameter @Bind.query(queryParameterName)Header@Bind.header(headerName)// Request Body
@Bind.body()

Queries and Data Models

Writing database queries by hand is error-prone and doesn’t leverage static analysis tools. Aqueduct’s ORM (Object-Relational Mapping) provides statically-typed queries that are easy to write and test.

Your application’s data model is defined by creating Dart classes. Each class is mapped to a database table, and each property of that class is mapped to a column in that table. Aqueduct’s command-line tool generates database migration files that detect changes in your data model that can be applied to a live, versioned database. A data model can also be represented as a JSON object to build tools on top of your application.

For more details, see the guide on Databases.

// This is a table definition of an 'article'
class _Article {
@Column(primaryKey: true)
int id;

String contents;

@Column(indexed: true)
DateTime publishedDate;
}

Authorization

OAuth 2.0 is a standardized authorization framework. Aqueduct contains a specification-compliant implementation of an OAuth 2.0 server that can be integrated directly into your application. This implementation is easily customizable — it can store authorization artifacts — like tokens and client identifiers — in different types of databases or use stateless authorization mechanisms like JWT. The default implementation leverages the Aqueduct ORM to store artifacts in PostgreSQL.

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 build our first API using aqueduct, 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.

--

--