Aqueduct #
Aqueduct is no longer actively developed. The last packages were released for Dart 2.x and aren’t compatible with Dart 3.x. If you’re starting a new project, use Conduit — the actively maintained official fork of Aqueduct with nearly identical APIs. This article remains relevant as a reference for understanding existing Aqueduct codebases and as a migration guide to Conduit.
Aqueduct was a highly influential Dart web server framework — introducing Dart-based ORM concepts, integrated OAuth2 authentication, and database migrations to the Dart ecosystem long before alternatives existed. Many tutorials, articles, and codebases on the internet still use Aqueduct. Understanding Aqueduct — especially its differences from Conduit — matters if you work with existing code or read old documentation.
A Brief History #
flowchart LR
A["Aqueduct\n2016-2021\nActively developed\nPopular in Dart 2.x"] -->|Not Dart 3 compatible\nMaintainer stopped| B["Abandoned\n2021+"]
B -->|Community fork\nActively developed| C["Conduit\n2021-present\nDart 3 compatible\nNearly identical API"]| Aspect | Aqueduct | Conduit |
|---|---|---|
| Status | ❌ Deprecated | ✅ Active |
| Dart 3 support | ❌ No | ✅ Yes |
| API | Original | Nearly identical |
| Package | aqueduct | conduit_core |
| CLI | aqueduct | conduit |
| Migration from Aqueduct | — | Easy (minimal changes) |
Aqueduct Core Concepts Still Relevant #
Although Aqueduct is deprecated, its concepts live on in Conduit. Understanding Aqueduct means understanding Conduit’s foundation:
ApplicationChannel #
// Aqueduct — ApplicationChannel
import 'package:aqueduct/aqueduct.dart';
class TokoChannel extends ApplicationChannel {
ManagedContext? context;
@override
Future prepare() async {
final config = TokoConfig(options!.configurationFilePath!);
final dataModel = ManagedDataModel.fromCurrentMirrorSystem();
final psc = PostgreSQLPersistentStore(
config.database.username,
config.database.password,
config.database.host,
config.database.port,
config.database.databaseName,
);
context = ManagedContext(dataModel, psc);
}
@override
Controller get entryPoint {
final router = Router();
router
.route('/produk/[:id]')
.link(() => ProdukController(context!));
return router;
}
}
// Conduit — nearly identical, only the import differs
import 'package:conduit_core/conduit_core.dart'; // not aqueduct
class TokoChannel extends ApplicationChannel {
// Exactly the same code as above
}
ResourceController #
// Aqueduct
import 'package:aqueduct/aqueduct.dart';
class ProdukController extends ResourceController {
ProdukController(this.context);
final ManagedContext context;
@Operation.get()
Future<Response> ambilSemua() async {
final query = Query<Produk>(context);
final hasil = await query.fetch();
return Response.ok(hasil.map((p) => p.asMap()).toList());
}
@Operation.get('id')
Future<Response> ambilSatu(@Bind.path('id') int id) async {
final query = Query<Produk>(context)
..where((p) => p.id).equalTo(id);
final produk = await query.fetchOne();
if (produk == null) return Response.notFound();
return Response.ok(produk.asMap());
}
}
// Conduit — IDENTICAL, only the import differs
import 'package:conduit_core/conduit_core.dart';
class ProdukController extends ResourceController {
// Exactly the same code
}
ManagedObject / ORM #
// Aqueduct — model definition
import 'package:aqueduct/aqueduct.dart';
class Produk extends ManagedObject<_Produk> implements _Produk {}
class _Produk {
@primaryKey
int? id;
String? nama;
double? harga;
bool? aktif;
}
// Conduit — IDENTICAL
import 'package:conduit_core/conduit_core.dart';
class Produk extends ManagedObject<_Produk> implements _Produk {}
class _Produk {
@primaryKey
int? id;
String? nama;
double? harga;
bool? aktif;
}
The Aqueduct → Conduit Migration Guide #
Migrating from Aqueduct to Conduit is relatively easy because the APIs are nearly identical. What needs to change:
1. pubspec.yaml #
# BEFORE (Aqueduct)
dependencies:
aqueduct: ^4.0.0
dev_dependencies:
aqueduct_test: ^4.0.0
# AFTER (Conduit)
dependencies:
conduit_core: ^4.3.0
conduit: ^4.3.0
dev_dependencies:
conduit_test: ^4.3.0
2. Import Statements #
// BEFORE — all imports from aqueduct
import 'package:aqueduct/aqueduct.dart';
import 'package:aqueduct_test/aqueduct_test.dart';
// AFTER — switch to conduit
import 'package:conduit_core/conduit_core.dart';
import 'package:conduit_test/conduit_test.dart';
The fastest way to replace all imports in a project:
# Replace all aqueduct imports with conduit at once
find lib test -name "*.dart" -exec sed -i \
"s/package:aqueduct\/aqueduct.dart/package:conduit_core\/conduit_core.dart/g" {} \;
find lib test -name "*.dart" -exec sed -i \
"s/package:aqueduct_test\/aqueduct_test.dart/package:conduit_test\/conduit_test.dart/g" {} \;
3. CLI Commands #
# BEFORE (Aqueduct CLI)
aqueduct create nama_project
aqueduct serve
aqueduct db generate
aqueduct db upgrade
aqueduct db upgrade --dry-run
# AFTER (Conduit CLI)
conduit create nama_project
conduit serve
conduit db generate
conduit db upgrade
conduit db upgrade --dry-run
4. Incompatible API Changes #
Some small changes that might exist:
// Some class names changed (rarely)
// Aqueduct
import 'package:aqueduct/managed_auth.dart';
class Pengguna extends ManagedObject<_Pengguna>
implements _Pengguna, ManagedAuthResourceOwner<_Pengguna> {}
// Conduit — different package name
import 'package:conduit_core/managed_auth.dart';
class Pengguna extends ManagedObject<_Pengguna>
implements _Pengguna, ManagedAuthResourceOwner<_Pengguna> {}
Aqueduct Features Available in Conduit #
All of Aqueduct’s main features are available in Conduit:
✅ ResourceController with @Operation and @Bind
✅ ManagedObject ORM
✅ Type-safe query builder
✅ Automatic database migrations
✅ OAuth2 server (AuthServer, AuthController, Authorizer)
✅ ApplicationChannel
✅ Router with path parameters
✅ Testing with Agent/TestClient
✅ YAML configuration
✅ Multiple isolate support
Full Example: Aqueduct Code to Migrate #
Here’s a complete Aqueduct code example alongside its Conduit version:
// AQUEDUCT — main.dart
import 'package:aqueduct/aqueduct.dart';
Future main() async {
final app = Application<TokoChannel>()
..options.configurationFilePath = 'config.yaml'
..options.port = 8888;
await app.start(numberOfInstances: 3, consoleLogging: true);
}
// CONDUIT — main.dart (identical)
import 'package:conduit_core/conduit_core.dart';
Future main() async {
final app = Application<TokoChannel>()
..options.configurationFilePath = 'config.yaml'
..options.port = 8888;
await app.start(numberOfInstances: 3, consoleLogging: true);
}
// AQUEDUCT — query.dart
import 'package:aqueduct/aqueduct.dart';
Future<List<Produk>> ambilProdukMahal(ManagedContext context) async {
return await (Query<Produk>(context)
..where((p) => p.harga).greaterThan(5000000)
..sortBy((p) => p.harga, QuerySortOrder.descending)
..fetchLimit = 10
).fetch();
}
// CONDUIT — query.dart (identical)
import 'package:conduit_core/conduit_core.dart';
Future<List<Produk>> ambilProdukMahal(ManagedContext context) async {
return await (Query<Produk>(context)
..where((p) => p.harga).greaterThan(5000000)
..sortBy((p) => p.harga, QuerySortOrder.descending)
..fetchLimit = 10
).fetch();
}
When Is Old Aqueduct Documentation Still Relevant? #
Although Aqueduct is deprecated, there are situations where old Aqueduct documentation and tutorials are still useful:
STILL RELEVANT when:
✓ Maintaining an existing Aqueduct codebase that can't be migrated yet
✓ Reading old tutorials (2018-2021) — the concepts and patterns are the same
✓ Understanding Conduit's design decisions (Conduit inherits Aqueduct's philosophy)
✓ Learning from Aqueduct code examples — almost all of them apply to Conduit
NOT RELEVANT when:
✗ Starting a new project — use Conduit directly
✗ Following Aqueduct setup/installation — its CLI isn't Dart 3 compatible
✗ Using features specific to Aqueduct 3.x and below
Projects Still Using Aqueduct #
If you join a team with an old Aqueduct codebase, here’s the recommended approach:
1. If the project still runs on Dart 2.x — no need to migrate immediately
Aqueduct works fine on Dart 2.x. Prioritize business features first.
2. If you need to upgrade to Dart 3.x — migration to Conduit is unavoidable
Follow the migration guide above: replace imports, update pubspec.yaml, switch CLIs.
3. If the codebase is large and migration is risky — do it in one step
Conduit and Aqueduct can't run side by side (package conflicts),
so the migration must be done fully in a single step.
4. Test everything after migrating — there may be minor differences
Especially around authentication behavior and some query edge cases.
Summary #
- Aqueduct is deprecated and incompatible with Dart 3.x. For new projects, use Conduit — the actively maintained fork with a nearly identical API.
- Migrating to Conduit is very easy — change imports from
package:aqueduct/aqueduct.darttopackage:conduit_core/conduit_core.dart, updatepubspec.yaml, and replace theaqueductCLI withconduit.- All Aqueduct concepts apply to Conduit —
ApplicationChannel,ResourceController,ManagedObject,Query,AuthServer,Routerare all available with the same names and APIs.- Old Aqueduct tutorials and articles are still useful for learning concepts — just swap the package name when following code examples.
- The official Conduit documentation is at conduit.dart.io — more accurate and up-to-date than the old Aqueduct documentation.
- Don’t install Aqueduct for new projects — the package isn’t Dart 3.x compatible and won’t receive bug fixes or security updates.
- Existing Aqueduct codebases on Dart 2.x can be left as-is if there’s no urgent need to upgrade. Prioritize migration when you need Dart 3.x.
- The
aqueduct/managed_auth.dartimport in Aqueduct becomesconduit_core/managed_auth.dartin Conduit — one of the most common errors during migration.