MongoDB
Finch uses the mongo_dart package for MongoDB and wraps it with a small set of conventions — FinchDBConfig, DBCollection, and the DQ query builder — so your controllers never have to build raw connection strings or repeat boilerplate CRUD code.
This guide covers:
- Connecting and configuring the pool
- Writing a collection class with
DBCollection - The built-in helper methods every collection gets for free
- Building queries and aggregation pipelines with
DQ - Wiring a collection into a model and a controller
- Checking connection health (e.g. from a cron job)
1. Configuration
MongoDB is configured through FinchDBConfig, passed to dbConfig in FinchConfigs:
FinchConfigs configs = FinchConfigs(
dbConfig: FinchDBConfig(
enable: true,
host: env.get('MONGODB_CONNECTION', 'localhost'),
port: env.get('MONGODB_PORT', '27017'),
user: env.get('MONGODB_USER', 'root'),
pass: env.get('MONGODB_PASSWORD', 'password'),
dbName: env.get('MONGODB_NAME', 'my_app'),
auth: env.get('MONGODB_AUTH', 'admin'), // authentication source database
maxConnections: 10, // size of the connection pool (Db.pool)
),
);
| Field | Type | Description |
|---|---|---|
enable |
bool |
When false, app.mongoDb is never connected — useful to skip Mongo entirely on apps that only use MySQL/SQLite |
host, port, user, pass, dbName |
String |
Standard connection credentials. Note port is a String, not an int |
auth |
String |
The MongoDB authSource database (usually admin) |
maxConnections |
int |
How many pooled connections Db.pool opens (default 10) |
Finch builds the connection string for you:
mongodb://user:pass@host:port/dbName/?authSource=auth
If you don't pass a field explicitly, FinchDBConfig falls back to reading it from the environment itself (MONGO_CONNECTION, MONGO_PORT, MONGO_INITDB_DATABASE, MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD, MONGO_INITDB_ROOT_AUTH) — but it is clearer to pass your own env.get(...) calls as shown above so the variable names match the rest of your .env file. See Configuration for the full list of app-wide environment variables.
Connection pooling
Finch never opens a single MongoDB socket. On startup, DBManager calls:
Db.pool(List.filled(config.maxConnections, config.link))
That creates a pool of maxConnections independent connections, so concurrent requests are served from different sockets instead of queuing behind one connection. Raise maxConnections for high-traffic apps; the default of 10 is fine for most projects.
2. Accessing the Database
var db = app.mongoDb; // returns the mongo_dart Db instance (the pool)
Check connection status before running startup logic or scheduled jobs:
if (app.mongoDb.isConnected) {
// safe to query
}
This is the same pattern Finch's own cron jobs use — see Commands for the cron API.
3. DBCollection — the collection base class
DBCollection (from package:finch/finch_model.dart) is an abstract class you extend for every MongoDB collection in your app. It gives you the raw mongo_dart collection plus a set of ready-made helper methods, and it auto-creates the collection the first time it's instantiated if it doesn't already exist.
import 'package:finch/finch_model.dart';
import '../app.dart';
import '../models/example_model.dart';
class ExampleCollections extends DBCollection {
ExampleCollections() : super(db: app.mongoDb, name: 'example');
Future<ExampleModel> insertExample(ExampleModel model) async {
var res = await collection.insert(model.toJson());
return ExampleModel.fromJson(res);
}
Future<List<ExampleModel>> getAllExample({int? start, int? count}) async {
start = (start != null && start > 0) ? start : null;
var rows = await collection
.modernFind(limit: count, skip: start, sort: DQ.order('_id'))
.toList();
return ExampleModel.fromListJson(rows);
}
}
collectionis the underlyingmongo_dartDbCollection(db.collection(name)), for anything the helpers below don't cover.nameanddbare the constructor arguments you pass tosuper(...).
Built-in helper methods
Every subclass of DBCollection gets these methods without writing any code:
| Method | Signature | Description |
|---|---|---|
existId |
Future<bool> existId(String idField) |
true if a document with that _id exists. Returns false for an invalid ObjectId string instead of throwing |
exist |
Future<bool> exist(String field, Object value) |
true if any document has field == value |
getCount |
Future<int> getCount({String? field, Object? value, Map<String,Object?>? filter}) |
Counts documents, optionally filtered by a single field/value pair or a raw filter map |
isEmpty / isNotEmpty |
Future<bool> get |
Shortcut around getCount() == 0 |
delete |
Future<bool> delete(String id) |
Deletes one document by _id |
deleteAll |
Future<bool> deleteAll() |
Deletes every document in the collection — use with care |
copy |
Future<void> copy(String id) |
Duplicates a document (strips _id so Mongo assigns a new one) |
updateField |
Future<void> updateField(String id, String field, Object? value) |
Sets a single field on one document, only if the id exists |
updateFields |
Future<void> updateFields(String id, Map<String, dynamic> fields) |
Merges multiple fields into one document |
updateAllForField |
Future<void> updateAllForField({required String field, required Object? value, required Map<String,Object?>? filter}) |
Bulk-updates one field across every document matching filter |
Example — using the built-ins directly from a controller without writing any extra query code:
var col = ExampleCollections();
if (await col.exist('slug', 'my-slug')) {
return rq.renderError(409, message: 'Slug already exists');
}
var total = await col.getCount();
var isFirstRun = await col.isEmpty;
await col.updateField(id, 'title', 'New title');
await col.delete(id);
4. Querying with modernFind
modernFind is the recommended way to query a collection — it accepts a filter, sort, limit, and skip in one call:
// All documents, newest first
var rows = await collection
.modernFind(sort: DQ.order('_id', true))
.toList();
// Filtered, paginated
var rows = await collection
.modernFind(
filter: where.eq('slug', 'my-slug'),
limit: 10,
skip: 0,
)
.toList();
where is mongo_dart's own selector builder (where.eq, where.id, ...) and works alongside DQ.
5. The DQ query builder
DQ is a static helper class (re-exported from finch_model.dart) that produces plain Map<String, Object?> MongoDB query/aggregation fragments — it exists purely to make queries more readable than hand-written maps with $-prefixed keys.
Comparison & logical operators
DQ.eq('value') // 'value' — for direct equality
DQ.gt(18) // { '$gt': 18 }
DQ.gte(18) // { '$gte': 18 }
DQ.lt(65) // { '$lt': 65 }
DQ.lte(65) // { '$lte': 65 }
DQ.hasIn(['a', 'b']) // { '$in': ['a', 'b'] }
DQ.hasNin(['a', 'b']) // { '$nin': ['a', 'b'] }
DQ.and([cond1, cond2]) // { '$and': [cond1, cond2] }
DQ.or([cond1, cond2]) // { '$or': [cond1, cond2] }
DQ.field('age', DQ.gte(18)) // { 'age': { '$gte': 18 } }
Text matching
DQ.like('john') // { '$regex': 'john', '$options': 'i' } — case-insensitive contains
DQ.uncase('john') // { '$regex': '^john$', '$options': 'i' } — case-insensitive exact match
Both escape regex special characters in the input for you, so user-supplied search terms are safe to pass directly.
ID helpers
DQ.id('507f191e810c19729de860ea') // { '_id': ObjectId(...) } — from a String
DQ.oid(objectId) // { '_id': ObjectId(...) } — from an existing ObjectId
Putting it together, a typical filtered search:
var rows = await collection
.modernFind(
filter: DQ.and([
DQ.field('status', 'active'),
DQ.field('name', DQ.like(searchTerm)),
]),
sort: DQ.order('createdAt'),
limit: 20,
)
.toList();
Sorting, paging and counting
DQ.order('createdAt') // { 'createdAt': -1 } — descending by default
DQ.order('createdAt', false) // { 'createdAt': 1 } — ascending
DQ.sortField('createdAt', true) // { 'createdAt': -1 } — same, aggregation-stage friendly
DQ.limit(20) // { '$limit': 20 }
DQ.skip(40) // { '$skip': 40 }
DQ.count('total') // { '$count': 'total' }
Aggregation pipelines
For anything beyond a simple filter — joins, grouping, computed fields — build an aggregation pipeline with DQ.pipeline(...) and run it with collection.aggregateToStream or collection.modernAggregate from mongo_dart:
var pipeline = DQ.pipeline([
DQ.match([
DQ.field('status', 'active'),
]),
DQ.lookup(
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'user',
),
DQ.unwind(path: 'user', preserveNullAndEmptyArrays: true),
DQ.group({
'_id': DQ.$field('user.country'),
'total': DQ.sum('amount'),
}),
DQ.sort({'total': -1}),
DQ.limit(10),
]);
var results = await collection.aggregateToStream(pipeline).toList();
Available aggregation-stage builders:
| Method | Stage | Purpose |
|---|---|---|
DQ.match(List<Map>) |
$match |
Filter documents entering the pipeline |
DQ.group(Map) |
$group |
Group documents and compute aggregates |
DQ.lookup(from:, localField:, foreignField:, as:) |
$lookup |
Left-outer join with another collection |
DQ.unwind(path:, as:, preserveNullAndEmptyArrays:) |
$unwind |
Flatten an array field into separate documents |
DQ.project(Map) |
$project |
Include/exclude/compute output fields |
DQ.sort(Map) / DQ.sortList(List<Map>) / DQ.sortOne(field, desc) |
$sort |
Sort pipeline results |
DQ.limit(int) / DQ.skip(int) |
$limit / $skip |
Pagination within a pipeline |
DQ.count(field) |
$count |
Count documents at that pipeline stage |
DQ.sum(field) / DQ.sumQuery(query) |
$sum |
Sum a field (or a computed expression) inside $group |
DQ.cond(ifCond:, thenCond:, elseCond:) |
$cond |
Conditional expression |
DQ.dateToString(field:, format:, timezone:) |
$dateToString |
Format a date field as a string |
DQ.toDate(field) |
$toDate |
Cast a field to a date |
DQ.$field(name) |
— | Prefixes a field name with $ (e.g. '$name') for use inside expressions |
6. Model
Finch models are plain Dart classes with toJson/fromJson — there's no code generation involved, so you fully control the shape of the document:
class ExampleModel {
String? id;
String title;
String slug;
ExampleModel({this.id, required this.title, required this.slug});
Map<String, dynamic> toJson() => {
if (id != null) '_id': id,
'title': title,
'slug': slug,
};
factory ExampleModel.fromJson(Map<String, dynamic> json) => ExampleModel(
id: json['_id']?.toString(),
title: json['title'] ?? '',
slug: json['slug'] ?? '',
);
static List<ExampleModel> fromListJson(List<Map<String, dynamic>> list) =>
list.map(ExampleModel.fromJson).toList();
}
7. Using the Collection in a Controller
class HomeController extends Controller {
Future<String> exampleDatabase() async {
var col = ExampleCollections();
if (rq.isPost) {
var model = ExampleModel(
title: rq.get<String>('title', def: ''),
slug: rq.get<String>('slug', def: ''),
);
await col.insertExample(model);
return rq.redirect('/example/database');
}
var items = await col.getAllExample(count: 20);
rq.addParam('items', items.map((e) => e.toJson()).toList());
return rq.renderView(path: 'example/database');
}
}
8. Using MongoDB from a cron job
Because app.mongoDb is a single shared pool, it's safe to reach it from anywhere in your app, including scheduled tasks. Always guard with isConnected first:
app.registerCron(
FinchCron(
schedule: FinchCron.evryDay(2),
onCron: (index, cron) async {
if (app.mongoDb.isConnected) {
await ExampleCollections().deleteAll();
}
},
).start(),
);
See Commands for the full cron scheduling API, and Database Migration for schema/data migrations that also run against MongoDB.