Best Practices for Developing Aqueduct Applications

Keep Projects Separate

Because Dart is cross-platform, developers may use the same project for both an Aqueduct application and a client application. Avoid this. When projects share dependencies, it may force one of them to use older versions of libraries. Two projects in one also creates a more confusing codebase.

Use Test Driven Development (or something close to it)

In Aqueduct, testing is a first-class citizen. The preferred method of development is to write tests using TestClient - as opposed to using a client application to 'eyeball' test an endpoint in development.

Developers should at minimum write tests for the 'success case' of an endpoint and should use automated testing to verify that code written in the future does not impact code written in the past. Aqueduct has behavior and tooling for testing applications that interface with a database or other external services.

Use a bin Script to Verify Assumptions

Keep a simple Dart script file in the bin/ directory that imports your project. Use this file as a scratchpad to test exploratory code before committing to a test suite. Don't check this file into source control.

import 'package:myapp/myapp.dart';

Future main() async {
  var whatIsThis = await someYetToBeNamedUsefullyMethod();
  print("$whatIsThis");
}

Create New Projects from a Template

Use aqueduct create to create applications with the appropriate structure and boilerplate. There are templates for different kinds of applications; view these templates with aqueduct create list-templates.

Use a Debugger

Use a debugger while running tests to stop execution, view variable values and verify program flow. Right-click on this file and select 'Debug' to run the tests in the debugger. Use breakpoints (by clicking on the gutter area to the left of the text editing area) to stop execution, view variable values and verify program flow.

Each project also has a bin/main.dart script to run the application without aqueduct serve. You may also right-click on this file and select 'Debug' to run the application with the debugger turned on. Use a client application, CURL or tools like Paw and Postman to issue requests while the debugger is running. Use aqueduct document to generate an OpenAPI specification that can be imported into HTTP client applications like Postman.

Use the Suggested Project Directory Structure

See Aqueduct Project Structure.

Pass Services to Controllers in setupRouter

Pass service objects to a controller in setupRouter and only pass the services the controller will use.

@override
void setupRouter(Router router) {
  router.route("/data").generate(() => new DBController(databaseConnection));
  router.route("/github").generate(() => new GitHubController(githubService));
}

These objects are called dependencies. Passing references like this allows for injecting dependencies that depend on the environment; e.g. in production, development or during tests. It also avoids tight coupling between the objects in your application.

Minimize the access a controller has to its dependencies; e.g. don't pass it a StreamController when it only needs Sink or a Stream.

Use a Test Harness

The test harness available in projects generated by the template makes testing considerably easier. It will require less typing and cleaner, safer test code.

Use config.src.yaml

Use the convention of config.src.yaml file to prevent configuration errors and inject test dependencies.

Understand how Aqueduct Uses Isolates

See more in Application Structure.

Use HTTPController Subclasses

Subclassing HTTPController provides significant conveniences, safeties and behaviors used by the majority of an application's request handling logic. Prefer to use this class for non-middleware controllers.

Keep RequestSink Tidy

A RequestSink should handle initialization, routing and nothing more. Consider moving non-initialization behavior into a service object in a separate file.

Avoid Raw SQL Queries

Prefer to use the Aqueduct ORM. It sends appropriate HTTP responses for different kinds of errors, validates input data and is ensures the queries match up with your data model.

Use API Reference

Aqueduct is an object oriented framework - behaviors are implemented by instances of some type. The types of objects, their properties and their behaviors all follow similar naming conventions to make the API more discoverable.

Most types in Aqueduct have a prefix in common with related types. For example, types like AuthServer, AuthStorage and AuthCode are all related because they deal with authentication and authorization. Methods are named consistently across classes (e.g, asMap is a common method name).

When looking for a solution, look at the API reference for the objects you have access to. These objects may already have the behavior you wish to implement or have a reference to an object with that behavior.

Use try-catch Sparingly

All request handling code is wrapped in a try-catch block that will interpret exceptions and errors and return meaningful HTTP responses. Unknown exceptions will yield a 500 Server Error response. In general, you do not need to use try-catch unless you want a different HTTP response than the one being returned for the exception.

Code that throws an exception during initialization should not be caught if the error is fatal to the application launching successfully.