Instant REST API’s and More— An Introduction to Angel Services

Tobe O
The Angel Framework
4 min readMar 4, 2017

Any modern Web application interacts with data, whether it be user-owned, server-owned, or fetched from an external source. Because we depend so much on data access, our most common use cases have evolved into libraries, frameworks, and even globally-recognized standards (such as REST). Angel, written with Google’s expressive Dart programming language, is a powerful Web application framework that provides high-level abstractions to expedite and simplify data access.

Services allow you to instantly turn data access classes into REST API’s, with no additional configuration. The service concept was borrowed from FeathersJS, and works even better in Dart, due to its built-in asynchrony and typing support. Service methods are mapped to RESTful routes, can be filtered with hooks, and can even be broadcasted across WebSockets. This pattern eliminates hundreds of lines of boilerplate code within your application, and helps you bring up prototypes and MVP’s in record time. At this time, Angel already has service implementations for in-memory, MongoDB, and RethinkDB data stores.

One Abstract Pattern

Services all extend from a single base class. Any method not implemented will automatically throw a 405 Method Not Allowed error:

class MyService extends Service {
// GET /
// Fetch all resources. Usually returns a List.
@override
Future index([Map params]);

// GET /:id
// Fetch one resource, by its ID
@override
Future read(id, [Map params]);

// POST /
// Create a resource. This endpoint should return
// the created resource.
@override
Future create(data, [Map params]);

// PATCH /:id
// Modifies a resource. Clients can submit only the data
// they want to change, and the corresponding resource will
// have only those fields changed. This endpoint should return
// the modified resource.
@override
Future modify(id, data, [Map params]);

// POST /:id
// Overwrites a resource. The existing resource is completely
// replaced by the new data. This endpoint should return the
// new resource.
@override
Future update(id, data, [Map params]);

// DELETE /:id
// Deletes a resource. This endpoint should return the
// deleted resource.
@override
Future remove(id, [Map params]);
}

This pre-defined pattern maps directly to REST, and also are easy to implement for a particular database. Some of the adapters that already exist include package:angel_mongo and package:angel_rethink.

Instant Routes, WebSockets, and More

The use method on Angel server instances registers a service instance as a singleton, and also runs the service’s addRoutes method. For example, to mount a REST API over a MongoDB collection:

app.use('/todos', new MongoService(db.collection('todos'));

Afterwards, visiting /todos in our Web browser will respond with JSON data fetched from the service’s index method:

[{"text":"Clean your room!","completed":false}, ...]

With package:angel_websocket, you can even broadcast service events across WebSockets:

configureServer(Angel app) async {
Db db = app.container.make(Db);
await app.configure(new AngelWebSocket());
await app.configure(Foo.configureServer());
await app.configure(Bar.configureServer());
await app.configure(Baz.configureServer(db));
}

Data from services can even be transformed into Dart objects.

Hooks

Service hooks allow you to filter or react to service methods and events without changing the functionality of service classes themselves. Hooks work on any service, and thus make it easier to implement universal solutions for common concerns, such as authorization logic. Calling use on your server wraps a service in a HookedService instance by default, so adding hooks is simple:

// Validation logic is separate from data access
app.service('users').beforeCreated.listen((e) {
if (!validateUserData(e.data))
throw new AngelHttpException.badRequest();
});

(Note: Validation support is also available for Angel, and is based on the same library as Dart’s package:test. Plus, it works in both client and browser.)

Using Services in Route Handlers

If you obtain a reference to one of your services within a route handler, you can manipulate data on the server-side:

app.get('/users/:id/name', (RequestContext req, res) async {
// Obtain a reference to the service singleton
var userService = app.service('users');

// Read a user based on route parameter
var user = await userService.read(req.params['id']);

// JSON response
return { 'name': user.name };
});

Regardless of which adapter you are using, the Service API remains the same. Even if you suddenly switch from one database to another, you will not have to change any code within your application’s routes.

Client-side Access

Angel also provides a convenient client-side library with the same service API as the server. This abstracts away HTTP requests or WebSocket exchanges, and also shrinks the framework’s overall learning curve. You only have to learn the API one time, rather than having to decipher both backend and frontend documentation.

import 'dart:html';
import 'package:angel_client/browser.dart';
final Angel app = new Rest(window.location.origin);
main() async {
var service = app.service('todos');
var todoList = await service.index(); // Sends an XHR and parses JSON
$ul.items.addAll(todoList.map((todo) => new LIElement()..text = todo['text']));
}

The WebSocket library also includes a WebSocket-based client implementation. Both the main client package and the WebSocket client (extends the main one) run both in the browser, and in the Dart VM.

Services are a convenient pattern for isolating your data logic, and let you spend more time on other areas of your application. Use them to their fullest potential, and your development time will shrink considerably.

You’ve probably noticed just by looking at the boilerplate that there is much more to the Angel framework than just service. Go ahead and explore it for yourself. Feedback is greatly appreciated!

--

--