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);
  }
}
  • collection is the underlying mongo_dart DbCollection (db.collection(name)), for anything the helpers below don't cover.
  • name and db are the constructor arguments you pass to super(...).

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.