Using Angel with Angular2

Angular2 is the hottest way to build browser-facing applications with Dart. But did you know how easy it is to combine it with Angel for a pleasant full-stack development experience?

In this article, and its companion video, we build a note-taking application, using Dart end-to-end. Model files and validation logic are shared seamlessly across client and browser, and leveraging package:angel_client, we write Angular2 service classes that interact with our backend using virtually the same API as on the server-side.

Find the video on YouTube.

The project source can be found in this GitHub repo.

Project Setup

First, like with any Dart project, we must write a pubspec.yaml file, and then run pub get:

name: angel_note  
dependencies:  
  angel_common: ^1.0.0
  angel_security: ^1.0.0
  angular2: ^3.0.0
dev_dependencies:  
  browser: ^0.10.0
  dart_to_js_script_rewriter: ^1.0.0
transformers:  
  - angular2:
      platform_directives:    - 'package:angular2/common.dart#COMMON_DIRECTIVES'
      platform_pipes:
       - 'package:angular2/common.dart#COMMON_PIPES'
      entry_points:
        - web/main.dart
  - dart_to_js_script_rewriter

Next, we'll want to create a default configuration file for our application, rather than hard-coding sensitive values in. In a file named config/default.yaml, type:

mongo_db: mongodb://localhost:27017/angel_notes  
# The JWT key specifically doesn't matter, just needs to be a string.
jwt_key: A73hfYWnaC1kxABqkcqmaD32mfHAAQ2  

Next, we'll want to create our models: User and Note. We can use the same model files on client and server. Hooray!

import 'package:angel_framework/common.dart';

class User extends Model {  
  @override
  String id;
  @override
  DateTime createdAt, updatedAt;
  String username, password;

  User({this.id, this.username, this.password, this.createdAt, this.updatedAt});

  static User parse(Map map) => new User(
      id: map['id'],
      username: map['username'],
      password: map['password'],
      createdAt:
          map['createdAt'] != null ? DateTime.parse(map['createdAt']) : null,
      updatedAt:
          map['updatedAt'] != null ? DateTime.parse(map['updatedAt']) : null);

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'username': username,
      'password': password,
      'createdAt': createdAt?.toIso8601String(),
      'updatedAt': updatedAt?.toIso8601String()
    };
  }
}

class Note extends Model {  
  @override
  String id;
  @override
  DateTime createdAt, updatedAt;
  String userId, title, text;
  // ...
}

Finally, we need validators:

import 'package:angel_validate/angel_validate.dart';

final Validator USER = new Validator({'username,password': isNonEmptyString});

final Validator CREATE_USER = USER.extend({})  
  ..requiredFields.addAll(['username', 'password']);

final Validator NOTE = new Validator({'title,text': isNonEmptyString});

final Validator CREATE_NOTE = NOTE.extend({})  
  ..requiredFields.addAll(['title', 'text']);

The API

Now that the boilerplate is out of the way, we can create the actual API server.

In lib/angel_note.dart:

import 'dart:async';  
import 'dart:convert';  
import 'dart:io';  
import 'package:angel_common/angel_common.dart';  
import 'package:angel_framework/hooks.dart' as hooks;  
import 'package:angel_security/hooks.dart' as auth;  
import 'package:mongo_dart/mongo_dart.dart';  
import 'models.dart';  
import 'validators.dart';

Future<Angel> createServer() async {  
  var app = new Angel();
  await app.configure(loadConfigurationFile());
  app.lazyParseBodies = true;
  app.injectSerializer(JSON.encode);

  var db = new Db(app.mongo_db);
  await db.open();

  app.use('/api/users', new MongoService(db.collection('users')));
  app.use('/api/notes', new MongoService(db.collection('notes')));

// Authentication setup
var auth = new AngelAuth(jwtKey: app.jwt_key, allowCookie: false);  
  auth.strategies
      .add(new LocalAuthStrategy(localAuthVerifier(app.service('api/users'))));
  auth.serializer = (User user) async => user.id;
  auth.deserializer =
      (String id) => app.service('api/users').read(id).then(User.parse);
  await app.configure(auth);

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

  app.chain(validate(CREATE_USER)).post('/api/signup',
          (RequestContext req, res) async {
        var body = await req.lazyBody();
        var user = await app.service('api/users').create(body);
        return user;
      });

  await app.configure(new PubServeLayer());

  // Static server, push state...
  var vDir = new VirtualDirectory();
  await app.configure(vDir);

  var indexFile = new File.fromUri(vDir.source.uri.resolve('index.html'));
  app.after.add((req, ResponseContext res) => res.sendFile(indexFile));
  app.responseFinalizers.add(gzip());
}

// ...

The source code for configureUsers and configureNotes has been omitted, but it can be found here. They are functions that configure permissions and hooks for the api/users and api/notes services.

Facilitating a Frontend

There are two sections of code that make it very simple to develop our Angular2 app alongside an Angel backend.

Firstly, we attach a PubServeLayer plug-in to our server. It comes from package:angel_proxy, and acts as a reverse proxy over another server, namely pub serve. 404's and failures to connect are caught, and allow you to still run other handlers, such as a static server or a push state fallback. Most importantly, a PubServeLayer automatically disables itself in production, so it is a friendly addition to a dev environment.

Next, we attach a VirtualDirectory, from package:angel_static. As you can imagine, it serves static files out of a directory. If we do not define one, then it chooses either web/ or build/web/, depending on whether we are in production. This means that we can always serve the right version of our frontend app. If you use a CachingVirtualDirectory, you can speed up your app's performance, because in addition to serving static files, it also sends Cache-Control headers, and supports ETags and If-Modified-Since.

Finally, we resolve an indexFile from the static server's resolved source directory. We add a final fallback route that serves the index.html file. As you can imagine, this is how we support push state routing within Angel.

Building an SPA

Now, we're 100% ready to serve an Angular2 frontend. So let's build it!

Services

Angel ships with package:angel_client, which allows you to query Angel backends using virtually the same API as on the server-side. It also supports authentication and WebSockets. Write a BackendService to use a Rest client within your app:

import 'dart:html';  
import 'package:angel_client/browser.dart';  
import 'package:angular2/angular2.dart';

@Injectable()
class BackendService {  
  final Map<String, Service> _services = {};
  final Angel app = new Rest(window.location.origin);

  Service service(String path) =>
      _services.putIfAbsent(path, () => app.service(path));
}

Writing an AuthService is equally straightforward. Because we can resume sessions out of localStorage, add an onLogin stream so that we can automatically load content (i.e. notes) when the user is logged in:

@Injectable()
class AuthService {  
  final BackendService _backend;
  final ErrorService _error;
  final Router _router;
  final StreamController<User> _onLogin =
      new StreamController<User>.broadcast();
  User _user;

  AuthService(this._backend, this._error, this._router) {
    // Resume a session, maybe...
    _backend.app.authenticate().then(handleAuth).catchError((_) {
      // Fail silently if we can't auto-login...
    });
  }

  Stream<User> get onLogin => _onLogin.stream;
  User get user => _user;

  Future handleAuth(AngelAuthResult auth) async {
    _onLogin.add(_user = User.parse(auth.data));
  }

  Future login(String username, String password) {
    return _backend.app.authenticate(
        type: 'local',
        credentials: {'username': username, 'password': password}).then((auth) {
      return handleAuth(auth).then((_) {
        _router.navigate(['/NoteList']);
      });
    }).catchError(_error.handleError);
  }

The full source code can be found here.

We can inject our backend into a NoteService that queries our /api/notes REST API with proper authentication:

@Injectable()
class NoteService {  
  final BackendService _backend;
  final ErrorService _error;
  final AuthService _auth;
  final Router _router;
  Service _service;
  bool _loaded = false;
  final List<Note> notes = [];

  NoteService(this._backend, this._error, this._router, this._auth) {
    _service = _backend.service('api/notes');

    // Auto-load notes on login, i.e. resume session
    _auth.onLogin.listen((_) {
      if (_loaded == false)
        fetchNotes();

    });
  }

  void fetchNotes() {
    _service.index().then((List<Map> notes) {
      this.notes
        ..clear()
        ..addAll(notes.map(Note.parse));
      _loaded = true;
    }).catchError(_error.handleError);
  }

This source code has been truncated as well. Find it here.

Use in Components

Now that we've written our service classes, we can easily inject them into components and access our API through a familiar, type-safe interface:

@Component(
    selector: 'note-list',
    templateUrl: 'note_list.html',
    directives: const [ROUTER_DIRECTIVES])
class NoteListComponent implements OnActivate, OnInit {  
  final AuthService auth;
  final NoteService _noteService;

  NoteListComponent(this.auth, this._noteService);

  List<Note> get notes => _noteService.notes;

  @override
  routerOnActivate(ComponentInstruction nextInstruction,
      ComponentInstruction prevInstruction) {
    if (auth.user != null)
      _noteService.fetchNotes();
  }

Conclusion

Angel was born out of a need for a server-side Dart framework suitable for full-stack development, so it provides functionality out-of-the-box that lets you simultaneously build client-side and server-side applications. When combined with Angular2, Angel can be used to build end-to-end Dart apps in record time.

Try out the framework (maybe star the GitHub repo?)!

Feedback+issues are welcome. :)