Developing RESTful APIs with Angel


It’s faster and easier than ever to build fast, secure API’s with Angel.

This post is, more or less, a response to Prosper Otemuyiwa’s recent post about Lumen.

View the source code for this example here.

Contents

Angel is a full-featured full-stack Web application framework in Dart I began working on in early 2016. As the project approaches its second birthday, I have released the latest installment: Angel v1.1.0. The framework got cleaner, more consistent, and not to mention – faster. Not only is the software fast, but the development process is, too. We can bring up an entire secure RESTful API in a matter of minutes.

Get Started

Assuming you have the Dart SDK installed, begin by creating a new directory and creating a pubspec.yaml file:

name: authors_rest_apidependencies:   angel_auth: ^1.1.0  angel_configuration: ^1.1.0  angel_file_service: ^1.0.0  angel_framework: ^1.1.0  angel_hot: ^1.1.0  angel_model: ^1.0.0  angel_security: ^1.1.0  angel_serialize: ^1.0.0  angel_validate: ^1.0.0dev_dependencies:   angel_serialize_generator: ^1.0.0  angel_test: ^1.1.0  build_runner: ^0.5.0  console: ^2.2.4  test: ^0.12.0

Create a file named lib/authors_rest_api.dart; we’ll eventually put our business logic here.

import 'dart:async';import 'package:angel_framework/angel_framework.dart';Future configureServer(Angel app) async {}

In bin/dev.dart, write a simple script that hot-reloads (!!!) our server as we change our code:

import 'dart:io';import 'package:angel_framework/angel_framework.dart';import 'package:angel_hot/angel_hot.dart';import 'package:authors_rest_api/authors_rest_api.dart' as authors_rest_api;main() async {  var hot = new HotReloader(() async {    var app = new Angel();    await app.configure(authors_rest_api.configureServer);    return app;  }, [    new Directory('lib'),  ]);  var server = await hot.startServer('127.0.0.1', 3000);  print('Listening at http://${server.address.address}:${server.port}');}

To run it:

dart --observe bin/dev.dart

The application running

Creating a Service – Instant REST API

An integral concept of the Angel framework, directly influenced by Feathers, is a service – an abstraction over a CRUD interface. When mounted on our server, RESTful API routes are created that map back to Service methods.

A Service can abstract over any data store. The following are already supported:

  • In-memory
  • Persistent JSON file
  • MongoDB
  • RethinkDB
  • PostgreSQL via a strongly-typed ORM

For the purposes of this example, we’ll mount a service at /api/authors that abstracts over a file named authors_db.json. This will create the following routes (and a few more):

  • GET /api/authors – List all authors
  • GET /api/authors/:id – Fetch a single author
  • POST /api/authors – Create an author
  • POST /api/authors/:id – Overwrite an author
  • PATCH /api/authors/:id – Modify an author
  • DELETE /api/authors/:id – Delete an author
import 'dart:async';import 'package:angel_file_service/angel_file_service.dart';import 'package:angel_framework/angel_framework.dart';import 'package:file/file.dart';import 'package:file/local.dart';const FileSystem fs = const LocalFileSystem();Future configureServer(Angel app) async {  HookedService service = app.use(    '/api/authors',    new JsonFileService(fs.file('authors_db.json')),  );}

Creating a user is as simple as POSTing to /api/authors:

User creation

Validation

We know better than to accept arbitrary input from users! Luckily, we can use package:angel_validate to easily define and enforce validation rules.

To apply a Validator to a service method, we need to use it as a service hook.

Add an import to package:angel_validate/server.dart, and add the following code:

Future configureServer(Angel app) async {  HookedService service = app.use(...);      var authorValidator = new Validator({    'email': isEmail,    'location': isAlphaDash,    'name,github,twitter,latest_article_published': isNonEmptyString,  });  var createAuthorValidator = authorValidator.extend({})    ..requiredFields.addAll([      'name',      'email',      'location',    ]);  // Validation on create  service.beforeCreated.listen(validateEvent(createAuthorValidator));  // Validation on modify+update (also validates create)  service.beforeModify(validateEvent(authorValidator));}

Any user input that is invalid will be rejected with a 400 Bad Request error.

Rejection of invalid data

Ensuring Unique Emails

We don’t want authors with duplicate email addresses to exist in our system. Let’s add another hook to the create event that throws a 403 if an email is duplicated:

service.beforeCreated.listen((e) async {    String email = e.data['email'], emailLower = email.toLowerCase();    // See if another author has this email.    Iterable existing = await e.service.index({      'query': {        'email_lower': emailLower,      }    });    if (existing.isNotEmpty) {      throw new AngelHttpException.forbidden(message: 'Somebody else has this email.');    }    // Otherwise, save the lowercased email.    e.data['email_lower'] = emailLower;});

Preventing Unauthorized API Access

The current state of our authors API is a rather precarious one – any rogue user on the Internet can manipulate our data with impunity. Though we have validation in place, anybody can spam the system with fake data, delete popular entries, or access information that should be private. The only way to prevent this is to implement authentication and authorization on our server. Fortunately, there are two packages that make this dead-simple:

Authentication

The AngelAuth class is modeled after Passport, and introduces a framework for JWT-based user authentication. Using the created JWT’s, we can associate requests with user objects, by sending an Authorization: Bearer <JWT here> HTTP header.

That being said, let’s create a dummy User class for the purposes of this example:

class User {  int id;  String username, password;  User({this.id, this.username, this.password});}

Now, let’s make an instance of AngelAuth, and configure it to serialize and deserialize users. Be sure to include this before your instantiantion of the authors service!

import 'dart:async';import 'package:angel_auth/angel_auth.dart';import 'package:angel_file_service/angel_file_service.dart';import 'package:angel_framework/angel_framework.dart';import 'package:angel_validate/server.dart';import 'package:file/file.dart';import 'package:file/local.dart';const FileSystem fs = const LocalFileSystem();Future configureServer(Angel app) async {  var auth = new AngelAuth<User>(    allowCookie: false,    // Typically, this will be loaded from a configuration file.    // However, this article doesn't cover configuration.    jwtKey: 'blQZtlqOBVffr6WWCXKyKwbuTpgk5E1K',  );  // The `serializer` typically returns a user's ID.  auth.serializer = (User user) async => user.id;  // The `deserializer` usually returns a lookup by ID.  // However, we're not covering that today.  //  // Check out this article for an explanation:  // https://medium.com/the-angel-framework/logging-users-in-to-angel-applications-ccf32aba0dac  auth.deserializer = (id) => new User(    username: 'angel',    password: 'framework',  );  // Configure the application to decode JWT's.  app.use(auth.decodeJwt);  await app.configure(auth.configureServer);    // .. Earlier service code omitted...}

Passport users will recognize strategies, which are authentication implementations for package:angel_auth. The package bundles LocalAuthStrategy, which supports username+password authentication:

 // Enable username+password authenticationvar localAuthStrategy = new LocalAuthStrategy((username, password) async {// In the real world, we wouldn't actually be using hard-coded credentials, but...if (username == 'angel' && password == 'framework')  return new User(    username: username,    password: password,  );});auth.strategies.add(localAuthStrategy);

Now, we just need to create an API route, /auth/local, that performs local authentication:

app.post('/auth/local', auth.authenticate('local'));

When the correct credentials are supplied, the response contains information about the authenticated user and the generated JWT.

Authentication response

Authorization

Authorization is typically less involved. Thanks to package:angel_auth and package:angel_security, all we have to do to prevent unauthorized writes is add the following code:

import 'package:angel_security/hooks.dart' as auth_hooks;Future configureServer(Angel app) async {  // ...    service.before([    HookedServiceEvent.created,    HookedServiceEvent.modified,    HookedServiceEvent.updated,    HookedServiceEvent.removed,  ], auth_hooks.restrictToAuthenticated());}

Unauthorized attempts are rejected with a 403 Forbidden error:
Rejection of unauthorized input

Conclusion

Just like Lumen, Angel makes it possible to bring up dynamic APIs using familiar constructs; however, Angel achieves this with much less boilerplate, and also the guarantees of a strongly-typed language with its own VM.