MongoDB stores JSON-like documents in collections instead of rows in tables. It suits data whose shape varies or grows over time: audit trails, event history, product attributes, telemetry. drivers/mongo wraps the official driver with typed collections, so a Coll[User] reads and writes User values. The step-by-step guide below the reference starts from zero.
go get github.com/AlexGalhardo/vukem/drivers/mongom, err := mongo.Open(ctx, os.Getenv("MONGO_URL"), os.Getenv("MONGO_DATABASE")) // empty MONGO_DATABASE: database from the URI path
app.AddHealthCheck("mongo", m.Ping) // reported by /up?checks=1
users := mongo.Collection[models.User](m, "users")
u, err := users.FindOne(ctx, bson.D{{Key: "email", Value: email}}) // mongo.ErrNotFound when missing
for u, err := range users.Find(ctx, bson.D{{Key: "active", Value: true}}, options.Find().SetLimit(50)) {
// break to stop early; the cursor is closed
}
id, err := users.Insert(ctx, u)
matched, modified, err := users.UpdateOne(ctx, filter, bson.D{{Key: "$set", Value: bson.D{{Key: "name", Value: n}}}})
totals, err := mongo.Aggregate[Total](ctx, users, pipeline)Also All, Count, InsertMany, UpdateMany, ReplaceOne, DeleteOne, DeleteMany, and Raw() on the client and on collections for indexes, transactions and change streams.
Connection#
| Variable | Meaning |
|---|---|
MONGO_URL | connection string; its path (mongodb://host:27017/app) names the database when MONGO_DATABASE is empty |
MONGO_DATABASE | database name passed as the third argument of mongo.Open; wins over the URI path |
Managed providers often give a URL without a path (Railway's MONGO_URL is mongodb://mongo:<password>@mongodb.railway.internal:27017). Set MONGO_DATABASE instead of editing the URL: with neither a path nor a name, Open fails.
Open pings the primary, so the app does not boot while MongoDB is down; a malformed URI always fails. To start anyway and let a readiness check report the outage, use mongo.OpenLazy, which validates the URI and database name without contacting the server:
m, err := mongo.OpenLazy(os.Getenv("MONGO_URL"), os.Getenv("MONGO_DATABASE"))
app.AddHealthCheck("mongo", m.Ping) // /up?checks=1 fails until MongoDB answers
app.OnShutdown(m.Close)The lazy client starts its topology-monitoring goroutines immediately, even before the server is up, so it must be closed on shutdown like any other client.
Server selection waits 10 seconds unless the URI sets serverSelectionTimeoutMS.
MongoDB has its own API on purpose: the SQL query builder and migrations do not apply to it.
Step by step: an audit log#
1. Start MongoDB locally
services:
mongo:
image: mongo:8.0
ports: ["27017:27017"]docker compose up -d mongoMONGO_URL=mongodb://localhost:27017
MONGO_DATABASE=app2. Describe the document
import "go.mongodb.org/mongo-driver/v2/bson"
type AuditEntry struct {
ID bson.ObjectID `bson:"_id,omitempty" json:"id"`
UserID int64 `bson:"user_id" json:"user_id"`
Action string `bson:"action" json:"action"`
Data map[string]any `bson:"data" json:"data"`
At time.Time `bson:"at" json:"at"`
}omitempty on _id lets MongoDB generate the id on insert.
3. Connect at boot and create an index
import (
"go.mongodb.org/mongo-driver/v2/bson"
mongodriver "go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"github.com/AlexGalhardo/vukem/drivers/mongo"
)
type Log struct {
entries *mongo.Coll[models.AuditEntry]
}
func New(ctx context.Context, app *vukem.App) (*Log, error) {
m, err := mongo.Open(ctx, app.Source.Get("MONGO_URL"), app.Source.Get("MONGO_DATABASE"))
if err != nil {
return nil, err
}
app.AddHealthCheck("mongo", m.Ping)
app.OnShutdown(m.Close)
entries := mongo.Collection[models.AuditEntry](m, "audit_entries")
// Indexes are created through the official driver: Raw() returns it.
_, err = entries.Raw().Indexes().CreateOne(ctx, mongodriver.IndexModel{
Keys: bson.D{{Key: "user_id", Value: 1}, {Key: "at", Value: -1}},
})
if err != nil {
return nil, err
}
return &Log{entries: entries}, nil
}CreateOne does nothing when the same index already exists, so it is safe on every boot.
4. Write and read
func (l *Log) Record(ctx context.Context, userID int64, action string, data map[string]any) error {
_, err := l.entries.Insert(ctx, models.AuditEntry{UserID: userID, Action: action, Data: data, At: time.Now().UTC()})
return err
}
func (l *Log) Recent(ctx context.Context, userID int64) ([]models.AuditEntry, error) {
return l.entries.All(ctx,
bson.D{{Key: "user_id", Value: userID}}, // built from a typed value, never from request JSON
options.Find().SetSort(bson.D{{Key: "at", Value: -1}}).SetLimit(50),
)
}5. Use it from a handler
func (h *Account) Activity(c *web.Ctx) error {
u, err := auth.User[models.User](c, "web")
if err != nil {
return err
}
entries, err := h.audit.Recent(c.Context(), u.ID)
if err != nil {
return err
}
return c.JSON(http.StatusOK, entries)
}Pitfalls#
- Database name. With neither a path in
MONGO_URLnorMONGO_DATABASE,Openfails; a path in the URI also changesauthSource(see the warning above). - No migrations. Collections are created on first insert. Create indexes in code at boot (as above) or in a deploy script; a missing index turns queries into full scans.
- Unique constraints need a unique index. Without one, two concurrent inserts both succeed. Add
Options: options.Index().SetUnique(true)and checkmongodriver.IsDuplicateKeyError(err). - Zero values are written. A
0or""field is stored unless the tag hasomitempty; withomitemptyyou cannot store an explicit zero. Choose per field. - Times are stored in UTC milliseconds. Nanoseconds are truncated; compare times at millisecond precision in tests.
- Large
Findresults.Allloads everything into memory; iterate withFind(a cursor) and set a limit for big collections.
Security notes#
- Operator injection. Decoding request JSON into a
bson.Mfilter lets a client send{"email": {"$ne": ""}}and match every document. Validate input withzinto typed fields first, then buildbson.Dfilters from those fields. - Enable authentication on every MongoDB outside your laptop, use a user with access to the app's database only, and keep the credentials in
MONGO_URLin the environment. - Use TLS (
mongodb+srv://on Atlas, or?tls=true) across networks.