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| Aspect | Shelf | Angel3 | Conduit |
|---|---|---|---|
| Routing | Manual | ✓ 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 |
| Philosophy | Composable | Express-like | Aqueduct-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:
| Framework | Status | Philosophy | Best for |
|---|---|---|---|
| Shelf | ✅ Active | Composable middleware | Microservices, lightweight APIs, full control |
| Conduit | ✅ Active | Aqueduct-style, ORM+Auth | Complex APIs with database and auth |
| Angel3 | ✅ Active | Express-like, Service+DI | MVC apps, developers familiar with Express/Nest |
| Aqueduct | ❌ Deprecated | Same as Conduit | Old 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 oldangel_*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
bool—trueto continue to the next handler,falseto stop the chain. More explicit than Shelf’s null/Response.@Exposefor declarative routing in controllers — the@Exposeprefix at the class level + the path at the method level form the complete URL.AngelAuthwith 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.