Angel

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_*, bukan angel_* 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 booltrue untuk lanjutkan ke handler berikutnya, false untuk hentikan chain. Lebih eksplisit dari Shelf yang menggunakan null/Response.
  • @Expose untuk routing deklaratif di controller — prefix dari @Expose di level kelas + path di level method membentuk URL lengkap.
  • AngelAuth dengan 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.

← Sebelumnya: Aqueduct   Berikutnya: ORM Dart →

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