Angel #

Angel (or Angel3, the Dart 3-compatible version) is a Dart web server framework inspired by Express.js and traditional MVC frameworks — with an emphasis on the service layer, dependency injection, and modularity. Unlike Conduit, which follows the Aqueduct pattern, Angel has its own philosophy: all data entities are accessed through services that can be implemented on top of any database, and components can be injected declaratively. It suits developers familiar with the Express/Nest.js paradigm.

Angel vs Conduit vs Shelf #

flowchart LR
    subgraph "Minimalist"
        S["Shelf\nComposable middleware\nNo opinionated structure\nMost flexible"]
    end
    subgraph "Full MVC"
        C["Conduit/Aqueduct\nIntegrated ORM\nBuilt-in OAuth2\nStrict patterns"]
    end
    subgraph "Express-style"
        A["Angel3\nService layer\nDependency injection\nModular & flexible"]
    end
AspectShelfAngel3Conduit
RoutingManual✓ Declarative✓ Declarative
DI Container✓ Built-in
Service Layer✓ Built-in
ORM✗ (manual)✓ Angel ORM✓ Built-in
Template Engine✓ Jinja-like
WebSocket✗ (manual)✓ Built-in✗ (manual)
Status✅ Active✅ Active (Angel3)✅ Active
PhilosophyComposableExpress-likeAqueduct-like

Installation #

# Install the Angel CLI
dart pub global activate angel3_cli

# Create a new project
angel3 init nama_project

# Or choose a template
angel3 init nama_project --template=basic
angel3 init nama_project --template=full  # with ORM and auth

# Run the development server
dart run bin/server.dart
# pubspec.yaml
dependencies:
  angel3_framework: ^8.0.0
  angel3_container: ^8.0.0
  angel3_middleware: ^8.0.0
  angel3_static: ^8.0.0
  angel3_auth: ^8.0.0
  angel3_orm: ^8.0.0              # optional ORM
  angel3_orm_postgres: ^8.0.0    # PostgreSQL adapter

dev_dependencies:
  angel3_test: ^8.0.0
  test: ^1.25.0

A Basic Application #

// bin/server.dart
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_framework/http.dart';
import 'package:logging/logging.dart';
import '../lib/src/routes/routes.dart';
import '../lib/src/config/config.dart';

void main() async {
  final app = Angel(
    logger: Logger('angel'),
    reflector: MirrorsReflector(),
  );

  // Configuration from a config file
  await app.configure(loadConfig());

  // Register routes
  await app.configure(configureRoutes);

  // Run the server
  final server = AngelHttp(app);
  await server.startServer('0.0.0.0', 3000);
  print('Angel server running at http://localhost:3000');
}
// lib/src/config/config.dart
import 'package:angel3_framework/angel3_framework.dart';

AngelConfigurer loadConfig() {
  return (Angel app) async {
    // Configuration from the environment or a file
    app.configuration['dbUrl'] =
        Platform.environment['DATABASE_URL'] ?? 'postgresql://localhost/toko';
    app.configuration['jwtSecret'] =
        Platform.environment['JWT_SECRET'] ?? 'dev_secret_change_in_prod';
  };
}

Routing #

Angel’s routing is closer to Express.js — handlers are plain functions:

// lib/src/routes/routes.dart
import 'package:angel3_framework/angel3_framework.dart';
import '../controllers/produk_controller.dart';
import '../controllers/auth_controller.dart';

AngelConfigurer configureRoutes = (Angel app) async {
  // Simple route with an inline handler
  app.get('/', (req, res) async {
    return res.json({'pesan': 'Welcome to the Angel API'});
  });

  // Route with parameters
  app.get('/hello/:nama', (RequestContext req, ResponseContext res) async {
    final nama = req.params['nama'] as String;
    return res.json({'salam': 'Hello, $nama!'});
  });

  // Group routes with a prefix
  app.group('/api', (router) {
    router.group('/produk', (router) {
      router.get('/', ProdukController().ambilSemua);
      router.get('/:id', ProdukController().ambilSatu);
      router.post('/', ProdukController().buat);
      router.put('/:id', ProdukController().perbarui);
      router.delete('/:id', ProdukController().hapus);
    });

    router.group('/auth', (router) {
      router.post('/login', AuthController().login);
      router.post('/register', AuthController().register);
    });
  });
};

Middleware in Angel #

// Middleware is a plain function or RequestHandler
Future<bool> authMiddleware(RequestContext req, ResponseContext res) async {
  final token = req.headers?.value('Authorization')?.replaceFirst('Bearer ', '');

  if (token == null || token.isEmpty) {
    res.statusCode = 401;
    await res.json({'error': 'Token required'});
    return false;  // false = stop the chain, don't continue to the next handler
  }

  // Verify the token
  try {
    final payload = verifikasiJWT(token);
    req.container!.registerSingleton<Map<String, dynamic>>(payload, as: 'currentUser');
    return true;  // true = continue to the next handler
  } catch (e) {
    res.statusCode = 401;
    await res.json({'error': 'Invalid token'});
    return false;
  }
}

// Apply middleware to a specific route
app.get('/profil', [authMiddleware, ProfilController().ambil]);

// Or to all routes in a group
app.group('/admin', [authMiddleware], (router) {
  router.get('/pengguna', AdminController().daftarPengguna);
  router.delete('/pengguna/:id', AdminController().hapusPengguna);
});

// Global middleware — applies to all routes
app.use(loggingMiddleware);
app.use(corsMiddleware);

Controllers #

Angel supports class-based controllers with route annotations:

// lib/src/controllers/produk_controller.dart
import 'package:angel3_framework/angel3_framework.dart';

@Expose('/produk')  // prefix for all routes in this controller
class ProdukController extends Controller {
  // Service injected automatically by the DI container
  @Inject(ProdukService)
  late ProdukService _service;

  @Expose('/', method: 'GET')
  Future<List<Map<String, dynamic>>> ambilSemua(RequestContext req) async {
    final halaman = int.tryParse(req.queryParameters['halaman'] ?? '1') ?? 1;
    final kategori = req.queryParameters['kategori'];
    return await _service.ambilSemua(halaman: halaman, kategori: kategori);
  }

  @Expose('/:id', method: 'GET')
  Future<Map<String, dynamic>?> ambilSatu(RequestContext req) async {
    final id = int.parse(req.params['id'] as String);
    final produk = await _service.ambilById(id);

    if (produk == null) {
      throw AngelHttpException.notFound(message: 'Product not found');
    }

    return produk;
  }

  @Expose('/', method: 'POST')
  Future<Map<String, dynamic>> buat(RequestContext req) async {
    final body = await req.parseBody();
    final data = req.bodyAsMap;
    return await _service.buat(data);
  }

  @Expose('/:id', method: 'PUT')
  Future<Map<String, dynamic>?> perbarui(RequestContext req) async {
    final id = int.parse(req.params['id'] as String);
    await req.parseBody();
    return await _service.perbarui(id, req.bodyAsMap);
  }

  @Expose('/:id', method: 'DELETE')
  Future<void> hapus(RequestContext req, ResponseContext res) async {
    final id = int.parse(req.params['id'] as String);
    await _service.hapus(id);
    res.statusCode = 204;
  }
}

The Service Layer — Angel’s Unique Concept #

Services are CRUD abstractions that can be implemented on top of various data sources — SQL databases, NoSQL, REST APIs, or in-memory:

// lib/src/services/produk_service.dart
import 'package:angel3_framework/angel3_framework.dart';

// Service interface — Angel defines Service<Id, Data>
abstract class ProdukService extends Service<int, Map<String, dynamic>> {}

// In-memory implementation (for testing/prototyping)
class InMemoryProdukService extends InMemoryService<int, Map<String, dynamic>>
    implements ProdukService {

  @override
  Future<Map<String, dynamic>> create(
    Map<String, dynamic> data, [
    Map<String, dynamic>? params,
  ]) async {
    data['id'] = items.length + 1;
    data['dibuatPada'] = DateTime.now().toIso8601String();
    return super.create(data, params);
  }
}

// PostgreSQL implementation
class PostgresProdukService extends Service<int, Map<String, dynamic>>
    implements ProdukService {
  final Pool _pool;

  PostgresProdukService(this._pool);

  @override
  Future<List<Map<String, dynamic>>> index([Map<String, dynamic>? params]) async {
    final result = await _pool.execute('SELECT * FROM produk WHERE aktif = true');
    return result.map((row) => row.toColumnMap()).toList();
  }

  @override
  Future<Map<String, dynamic>?> read(int id, [Map<String, dynamic>? params]) async {
    final result = await _pool.execute(
      r'SELECT * FROM produk WHERE id = $1',
      parameters: [id],
    );
    return result.isEmpty ? null : result.first.toColumnMap();
  }

  @override
  Future<Map<String, dynamic>> create(
    Map<String, dynamic> data, [
    Map<String, dynamic>? params,
  ]) async {
    final result = await _pool.execute(
      r'INSERT INTO produk (nama, harga, stok) VALUES ($1, $2, $3) RETURNING *',
      parameters: [data['nama'], data['harga'], data['stok']],
    );
    return result.first.toColumnMap();
  }

  @override
  Future<Map<String, dynamic>> update(
    int id,
    Map<String, dynamic> data, [
    Map<String, dynamic>? params,
  ]) async {
    final result = await _pool.execute(
      r'UPDATE produk SET nama = $2, harga = $3 WHERE id = $1 RETURNING *',
      parameters: [id, data['nama'], data['harga']],
    );
    return result.first.toColumnMap();
  }

  @override
  Future<Map<String, dynamic>> remove(int id, [Map<String, dynamic>? params]) async {
    final result = await _pool.execute(
      r'DELETE FROM produk WHERE id = $1 RETURNING *',
      parameters: [id],
    );
    return result.first.toColumnMap();
  }
}

// Register the service to the DI container in configuration
app.use('/api/produk', PostgresProdukService(pool));
// Angel automatically creates CRUD endpoints for this service!
// GET /api/produk → index()
// GET /api/produk/:id → read()
// POST /api/produk → create()
// PUT /api/produk/:id → update()
// DELETE /api/produk/:id → remove()

Dependency Injection #

Angel has an integrated DI container — components can be injected without manual initialization:

// Register dependencies
app.container.registerSingleton<DatabasePool>(
  DatabasePool(url: app.configuration['dbUrl'] as String),
);

app.container.registerSingleton<ProdukService>(
  PostgresProdukService(app.container.make<DatabasePool>()),
);

// Inject in controllers automatically
@Expose('/produk')
class ProdukController extends Controller {
  @Inject(ProdukService)
  late ProdukService produkService;  // injected automatically by the container

  @Expose('/')
  Future<List> ambilSemua() => produkService.index();
}

// Inject directly into handler functions
app.get('/info', (ProdukService svc, RequestContext req) async {
  final total = await svc.count();
  return {'total': total};
  // svc is resolved automatically from the DI container!
});

Authentication with Angel Auth #

import 'package:angel3_auth/angel3_auth.dart';
import 'package:angel3_framework/angel3_framework.dart';

AngelConfigurer configureAuth = (Angel app) async {
  final auth = AngelAuth<Pengguna>(
    serializer: (user) async => user.id.toString(),
    deserializer: (id) async => await PenggunaService().ambilById(int.parse(id)),
    jwtSecret: app.configuration['jwtSecret'] as String,
  );

  // Strategy: Local (username/password)
  auth.strategies['local'] = LocalAuthStrategy(
    (email, password) async {
      final pengguna = await PenggunaService().cariByEmail(email);
      if (pengguna == null) return null;
      if (!verifikasiPassword(password, pengguna.hashedPassword)) return null;
      return pengguna;
    },
    usernameField: 'email',
    passwordField: 'password',
  );

  // Strategy: JWT
  auth.strategies['jwt'] = JwtAuthStrategy(auth);

  await app.configure(auth.configureServer);

  // Login endpoint
  app.post('/auth/login', auth.authenticate('local', (req, res, user) async {
    final token = await auth.createJwt(req, res, user as Pengguna);
    return res.json({'token': token, 'pengguna': user.toJson()});
  }));

  // Protected route — requires a JWT
  app.get('/profil', [auth.authenticate('jwt')], (req, res) async {
    final user = req.container!.make<Pengguna>();
    return res.json(user.toJson());
  });
};

WebSocket in Angel #

Angel supports WebSocket natively with a higher-level protocol than raw WebSocket:

import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_websocket/server.dart';

AngelConfigurer configureWebSocket = (Angel app) async {
  final ws = AngelWebSocket(app);
  await app.configure(ws.configureServer);

  // Listen for events from clients
  ws.onConnection.listen((socket) {
    print('Client connected: ${socket.request?.connectionInfo?.remoteAddress}');

    socket.on['chat-pesan'].listen((data) {
      // Broadcast to all connected clients
      ws.batchEvent(WebSocketEvent(type: 'chat-pesan', data: data));
    });

    socket.on['bergabung-room'].listen((data) {
      socket.join(data['room'] as String);
      print('${socket} joined room: ${data['room']}');
    });
  });

  // Send events to everyone or to a specific room
  app.get('/broadcast/:pesan', (req, res) async {
    ws.batchEvent(WebSocketEvent(
      type: 'pengumuman',
      data: {'pesan': req.params['pesan']},
    ));
    return res.json({'status': 'terkirim'});
  });
};

Testing with Angel Test #

// test/produk_test.dart
import 'package:angel3_framework/angel3_framework.dart';
import 'package:angel3_framework/http.dart';
import 'package:angel3_test/angel3_test.dart';
import 'package:test/test.dart';
import '../lib/src/routes/routes.dart';

void main() {
  late Angel app;
  late AngelHttp http;
  late TestClient client;

  setUp(() async {
    app = Angel();
    await app.configure(configureRoutes);
    http = AngelHttp(app);
    client = await connectTo(app);
  });

  tearDown(() async {
    await client.close();
    await http.close();
  });

  group('GET /produk', () {
    test('returns a list of products', () async {
      final res = await client.get(Uri.parse('/api/produk'));
      expect(res.statusCode, equals(200));

      final body = await res.readAsJson() as List;
      expect(body, isList);
    });
  });

  group('POST /produk', () {
    test('creates a new product', () async {
      final res = await client.post(
        Uri.parse('/api/produk'),
        body: '{"nama": "Test", "harga": 10000}',
        headers: {'Content-Type': 'application/json'},
      );
      expect(res.statusCode, anyOf(equals(200), equals(201)));
    });
  });
}

Comparing Dart Server-Side Frameworks #

After covering Shelf, Conduit, Aqueduct, and Angel, here’s a summary to help you choose:

FrameworkStatusPhilosophyBest for
Shelf✅ ActiveComposable middlewareMicroservices, lightweight APIs, full control
Conduit✅ ActiveAqueduct-style, ORM+AuthComplex APIs with database and auth
Angel3✅ ActiveExpress-like, Service+DIMVC apps, developers familiar with Express/Nest
Aqueduct❌ DeprecatedSame as ConduitOld codebases, migrate to Conduit
CHOOSE Shelf if:
  ✓ You need full control over every aspect
  ✓ You're building small microservices
  ✓ You want composable, easily testable middleware

CHOOSE Conduit if:
  ✓ You need ORM, database migrations, and built-in OAuth2
  ✓ Your team is familiar with the Aqueduct pattern
  ✓ You're building a complete enterprise API

CHOOSE Angel3 if:
  ✓ You're familiar with Express.js, Nest.js, or Rails
  ✓ You need a service layer with a DI container
  ✓ You need WebSocket integrated with the framework
  ✓ You want to build complete web apps (with a template engine)

Summary #

  • Angel3 is the active Dart 3-compatible version of Angel — use the angel3_* packages, not the old angel_* ones.
  • Angel routing is similar to Express.js — handlers are plain functions ((req, res) async => ...), either inline or referenced as controller methods.
  • The service layer is Angel’s signature concept — CRUD abstractions implementable on top of any data source. app.use('/path', service) automatically creates all REST endpoints.
  • Dependency injection is integrated — components can be injected into handler functions and controller properties without manual configuration everywhere.
  • Middleware returns a booltrue to continue to the next handler, false to stop the chain. More explicit than Shelf’s null/Response.
  • @Expose for declarative routing in controllers — the @Expose prefix at the class level + the path at the method level form the complete URL.
  • AngelAuth with the strategy pattern — supports various authentication strategies (local, JWT, OAuth) that can be combined.
  • Built-in WebSocket with AngelWebSocket — supports rooms, events, and broadcasting to all or a subset of clients.
  • Framework choice depends on philosophy — Shelf for flexibility, Conduit for integrated ORM+auth, Angel3 for an Express-like experience with DI.
  • Angel3 documentation is at pub.dev packages with the angel3_ prefix — every component is split into a separate package you can install as needed.

← Previous: Aqueduct   Next: Dart ORM →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact