Angel #
Angel (atau Angel3, nama versi yang kompatibel dengan Dart 3) adalah framework web server Dart yang mengambil inspirasi dari Express.js dan framework MVC tradisional — dengan penekanan pada service layer, dependency injection, dan modularitas. Berbeda dari Conduit yang mengikuti pola Aqueduct, Angel memiliki filosofi tersendiri: semua entitas data diakses melalui service yang bisa diimplementasikan di atas database apapun, dan komponen-komponen bisa di-inject secara deklaratif. Cocok untuk developer yang familiar dengan paradigma Express/Nest.js.
Angel vs Conduit vs Shelf #
flowchart LR
subgraph "Minimalis"
S["Shelf\nMiddleware composable\nTidak ada opiniated structure\nPaling fleksibel"]
end
subgraph "Full MVC"
C["Conduit/Aqueduct\nORM terintegrasi\nOAuth2 bawaan\nPattern ketat"]
end
subgraph "Express-style"
A["Angel3\nService layer\nDependency injection\nModular & fleksibel"]
end
| Aspek | Shelf | Angel3 | Conduit |
|---|---|---|---|
| Routing | Manual | ✓ Deklaratif | ✓ Deklaratif |
| DI Container | ✗ | ✓ Bawaan | ✗ |
| Service Layer | ✗ | ✓ Bawaan | ✗ |
| ORM | ✗ (manual) | ✓ Angel ORM | ✓ Bawaan |
| Template Engine | ✗ | ✓ Jinja-like | ✗ |
| WebSocket | ✗ (manual) | ✓ Bawaan | ✗ (manual) |
| Status | ✅ Aktif | ✅ Aktif (Angel3) | ✅ Aktif |
| Filosofi | Composable | Express-like | Aqueduct-like |
Instalasi #
# Install Angel CLI
dart pub global activate angel3_cli
# Buat project baru
angel3 init nama_project
# Atau pilih template
angel3 init nama_project --template=basic
angel3 init nama_project --template=full # dengan ORM dan auth
# Jalankan 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 # ORM opsional
angel3_orm_postgres: ^8.0.0 # adapter PostgreSQL
dev_dependencies:
angel3_test: ^8.0.0
test: ^1.25.0
Aplikasi Dasar #
// 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(),
);
// Konfigurasi dari file config
await app.configure(loadConfig());
// Daftarkan routes
await app.configure(configureRoutes);
// Jalankan server
final server = AngelHttp(app);
await server.startServer('0.0.0.0', 3000);
print('Server Angel berjalan di http://localhost:3000');
}
// lib/src/config/config.dart
import 'package:angel3_framework/angel3_framework.dart';
AngelConfigurer loadConfig() {
return (Angel app) async {
// Konfigurasi dari environment atau 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 menggunakan routing yang lebih dekat ke Express.js — handler adalah fungsi biasa:
// 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 {
// Route sederhana dengan inline handler
app.get('/', (req, res) async {
return res.json({'pesan': 'Selamat datang di API Angel'});
});
// Route dengan parameter
app.get('/hello/:nama', (RequestContext req, ResponseContext res) async {
final nama = req.params['nama'] as String;
return res.json({'salam': 'Halo, $nama!'});
});
// Group routes dengan 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 di Angel #
// Middleware adalah fungsi atau RequestHandler biasa
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 diperlukan'});
return false; // false = hentikan chain, jangan lanjutkan ke handler berikutnya
}
// Verifikasi token
try {
final payload = verifikasiJWT(token);
req.container!.registerSingleton<Map<String, dynamic>>(payload, as: 'currentUser');
return true; // true = lanjutkan ke handler berikutnya
} catch (e) {
res.statusCode = 401;
await res.json({'error': 'Token tidak valid'});
return false;
}
}
// Terapkan middleware ke route tertentu
app.get('/profil', [authMiddleware, ProfilController().ambil]);
// Atau ke semua route dalam group
app.group('/admin', [authMiddleware], (router) {
router.get('/pengguna', AdminController().daftarPengguna);
router.delete('/pengguna/:id', AdminController().hapusPengguna);
});
// Global middleware — berlaku untuk semua route
app.use(loggingMiddleware);
app.use(corsMiddleware);
Controller #
Angel mendukung controller berbasis kelas dengan anotasi route:
// lib/src/controllers/produk_controller.dart
import 'package:angel3_framework/angel3_framework.dart';
@Expose('/produk') // prefix semua route di controller ini
class ProdukController extends Controller {
// Service di-inject secara otomatis oleh 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: 'Produk tidak ditemukan');
}
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;
}
}
Service Layer — Konsep Unik Angel #
Service adalah abstraksi CRUD yang bisa diimplementasikan di atas berbagai sumber data — database SQL, NoSQL, REST API, atau in-memory:
// lib/src/services/produk_service.dart
import 'package:angel3_framework/angel3_framework.dart';
// Interface service — Angel mendefinisikan Service<Id, Data>
abstract class ProdukService extends Service<int, Map<String, dynamic>> {}
// Implementasi in-memory (untuk testing/prototype)
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);
}
}
// Implementasi PostgreSQL
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();
}
}
// Daftarkan service ke DI container di konfigurasi
app.use('/api/produk', PostgresProdukService(pool));
// Angel otomatis membuat endpoint CRUD untuk service ini!
// 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 memiliki DI container yang terintegrasi — komponen bisa di-inject tanpa inisialisasi manual:
// Daftarkan dependencies
app.container.registerSingleton<DatabasePool>(
DatabasePool(url: app.configuration['dbUrl'] as String),
);
app.container.registerSingleton<ProdukService>(
PostgresProdukService(app.container.make<DatabasePool>()),
);
// Inject di controller secara otomatis
@Expose('/produk')
class ProdukController extends Controller {
@Inject(ProdukService)
late ProdukService produkService; // di-inject otomatis oleh container
@Expose('/')
Future<List> ambilSemua() => produkService.index();
}
// Inject langsung ke handler function
app.get('/info', (ProdukService svc, RequestContext req) async {
final total = await svc.count();
return {'total': total};
// svc di-resolve otomatis dari DI container!
});
Autentikasi dengan 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 — butuh JWT
app.get('/profil', [auth.authenticate('jwt')], (req, res) async {
final user = req.container!.make<Pengguna>();
return res.json(user.toJson());
});
};
WebSocket di Angel #
Angel mendukung WebSocket secara bawaan dengan protocol yang lebih tinggi dari 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 event dari client
ws.onConnection.listen((socket) {
print('Client terhubung: ${socket.request?.connectionInfo?.remoteAddress}');
socket.on['chat-pesan'].listen((data) {
// Broadcast ke semua client yang terhubung
ws.batchEvent(WebSocketEvent(type: 'chat-pesan', data: data));
});
socket.on['bergabung-room'].listen((data) {
socket.join(data['room'] as String);
print('${socket} bergabung ke room: ${data['room']}');
});
});
// Kirim event ke semua atau ke room tertentu
app.get('/broadcast/:pesan', (req, res) async {
ws.batchEvent(WebSocketEvent(
type: 'pengumuman',
data: {'pesan': req.params['pesan']},
));
return res.json({'status': 'terkirim'});
});
};
Testing dengan 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('mengembalikan list produk', () 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('buat produk baru', () 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)));
});
});
}
Perbandingan Framework Dart Server-Side #
Setelah membahas Shelf, Conduit, Aqueduct, dan Angel, berikut ringkasan untuk membantu memilih:
| Framework | Status | Filosofi | Cocok untuk |
|---|---|---|---|
| Shelf | ✅ Aktif | Composable middleware | Microservice, API ringan, kontrol penuh |
| Conduit | ✅ Aktif | Aqueduct-style, ORM+Auth | API kompleks dengan database dan auth |
| Angel3 | ✅ Aktif | Express-like, Service+DI | MVC app, developer familiar Express/Nest |
| Aqueduct | ❌ Deprecated | Sama dengan Conduit | Codebase lama, migrasikan ke Conduit |
PILIH Shelf jika:
✓ Butuh kontrol penuh atas setiap aspek
✓ Membangun microservice kecil
✓ Ingin composable middleware yang mudah di-test
PILIH Conduit jika:
✓ Butuh ORM, migration database, dan OAuth2 bawaan
✓ Tim familiar dengan pattern Aqueduct
✓ Membangun API enterprise yang lengkap
PILIH Angel3 jika:
✓ Familiar dengan Express.js, Nest.js, atau Rails
✓ Butuh service layer dengan DI container
✓ Butuh WebSocket yang terintegrasi dengan framework
✓ Ingin membangun aplikasi web lengkap (dengan template engine)
Ringkasan #
- Angel3 adalah versi aktif Angel yang kompatibel dengan Dart 3 — gunakan package
angel3_*, bukanangel_*yang lama.- Routing Angel mirip Express.js — handler adalah fungsi biasa (
(req, res) async => ...), bisa inline atau di-reference sebagai method controller.- Service layer adalah konsep khas Angel — abstraksi CRUD yang bisa diimplementasikan di atas sumber data apapun.
app.use('/path', service)otomatis membuat semua endpoint REST.- Dependency injection terintegrasi — komponen bisa di-inject ke handler function dan controller property tanpa konfigurasi manual di setiap tempat.
- Middleware mengembalikan
bool—trueuntuk lanjutkan ke handler berikutnya,falseuntuk hentikan chain. Lebih eksplisit dari Shelf yang menggunakan null/Response.@Exposeuntuk routing deklaratif di controller — prefix dari@Exposedi level kelas + path di level method membentuk URL lengkap.AngelAuthdengan strategy pattern — dukung berbagai strategi autentikasi (local, JWT, OAuth) yang bisa dikombinasikan.- WebSocket bawaan dengan
AngelWebSocket— mendukung room, event, dan broadcast ke semua atau subset client.- Pilihan framework tergantung filosofi: Shelf untuk fleksibilitas, Conduit untuk ORM+auth terintegrasi, Angel3 untuk Express-like experience dengan DI.
- Dokumentasi Angel3 ada di pub.dev packages dengan prefix angel3_ — setiap komponen dipecah menjadi package terpisah yang bisa diinstal sesuai kebutuhan.