NoSQL / MongoDB

Nimbus ships a first-class NoSQL layer with a unified Driver / Collection interface and a production-ready MongoDB implementation. The API keeps the same feel as the SQL side โ€” register a connection, grab it by name, and use a fluent query builder or the raw Collection methods.

Documentation

Installation

The fastest way to get started is with the CLI plugin command. It scaffolds the config, boot wiring, and environment variables for you:

nimbus plugin:install nosql

This single command:

  • Creates config/nosql.go with a NoSQLConfig struct
  • Adds a bootNoSQL(app) function in bin/server.go that connects MongoDB, registers the driver globally, and binds *nimbus.NoSQL into the service container
  • Appends MONGO_URI and MONGO_DATABASE to .env.example
  • Registers loadNoSQL() in your config/config.go

Manual Installation

If you prefer manual setup, pull in the NoSQL package and the MongoDB driver:

go get github.com/CodeSyncr/nimbus/database/nosql
go get go.mongodb.org/mongo-driver/v2

Configuration

Add MONGO_URI and MONGO_DATABASE to your .env:

MONGO_URI=mongodb://localhost:27017
MONGO_DATABASE=myapp

Scaffolded Config (plugin:install)

Running nimbus plugin:install nosql creates config/nosql.go with a dedicated config struct:

package config

var NoSQL NoSQLConfig

type NoSQLConfig struct {
    MongoURI       string
    MongoDatabase  string
    ConnectTimeout string // default: "10s"
    MaxPoolSize    uint64 // default: 100
    MinPoolSize    uint64 // default: 0
}

func loadNoSQL() {
    NoSQL = NoSQLConfig{
        MongoURI:       cfg("mongo.uri", ""),
        MongoDatabase:  cfg("mongo.database", "nimbus"),
        ConnectTimeout: cfg("mongo.connect_timeout", "10s"),
    }
}

The generated bootNoSQL function in bin/server.go wires everything automatically:

func bootNoSQL(app *nimbus.App) {
    if config.NoSQL.MongoURI == "" {
        return // NoSQL not configured โ€” skip silently
    }

    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    mongoDriver, err := nosql.ConnectMongo(ctx, nosql.MongoConfig{
        URI:      config.NoSQL.MongoURI,
        Database: config.NoSQL.MongoDatabase,
    })
    if err != nil {
        fmt.Fprintf(os.Stderr, "MongoDB connection failed: %v\n", err)
        os.Exit(1)
    }

    // Register in the NoSQL connection manager
    nosql.Register("mongo", mongoDriver)

    // Set the global *nimbus.NoSQL handle
    nimbus.SetNoSQL(mongoDriver)

    // Bind into the service container for DI
    app.Container.Singleton("nosql", func() *nimbus.NoSQL {
        return nimbus.GetNoSQL()
    })
}

Manual Boot (without plugin:install)

If you set up manually, connect in bin/server.go or your boot file:

import "github.com/CodeSyncr/nimbus/database/nosql"

mongoDriver, err := nosql.ConnectMongo(ctx, nosql.MongoConfig{
    URI:      config.Database.MongoURI,
    Database: config.Database.MongoDatabase,
})
if err != nil {
    log.Fatalf("mongo: %v", err)
}

nosql.Register("mongo", mongoDriver)
nimbus.SetNoSQL(mongoDriver) // enable *nimbus.NoSQL app-level access
MongoConfig fieldTypeDefaultDescription
URIstringโ€”MongoDB connection string
Databasestringโ€”Default database name
ConnectTimeouttime.Duration10sConnection timeout
MaxPoolSizeuint64100Max connections in pool
MinPoolSizeuint640Min idle connections

Using the Connection

Application-Level Access (*nimbus.NoSQL)

Just like *nimbus.DB for SQL, the framework provides *nimbus.NoSQL as a high-level handle. Use the global helper or resolve from the container:

// Global helper โ€” works anywhere
store := nimbus.GetNoSQL()                    // *nimbus.NoSQL
coll  := store.Collection("users")            // nosql.Collection

// Shorthand โ€” collection from the default connection
coll = nimbus.NoSQLCollection("users")

// Named connection (multi-db)
analytics := nimbus.NoSQLConnection("analytics") // *nimbus.NoSQL

Container Injection (Dependency Injection)

When bootNoSQL runs, it binds *nimbus.NoSQL into the service container. Resolve it in controllers or services:

type TodoController struct {
    Store *nimbus.NoSQL
}

func NewTodoController(app *nimbus.App) *TodoController {
    return &TodoController{
        Store: app.Container.MustMake("nosql").(*nimbus.NoSQL),
    }
}

func (tc *TodoController) Index(c *nimbus.Context) error {
    var todos []Todo
    tc.Store.Collection("todos").Find(c.Request.Context(), nosql.Filter{"active": true}, &todos)
    return c.JSON(todos)
}

Low-Level Package Access

You can also use the nosql package directly:

store := nosql.Connection("mongo")        // returns nosql.Driver
coll  := store.Collection("users")        // returns nosql.Collection

// Switch database at runtime
analytics := store.Database("analytics")  // same client, different DB
events    := analytics.Collection("events")

CRUD Operations

Insert

// Model definition โ€” embed nosql.Model just like database.Model for SQL
type User struct {
    nosql.Model
    Name  string
    Email string
    Age   int
}

coll := nimbus.NoSQLCollection("users")

// Insert one
res, err := coll.InsertOne(ctx, User{Name: "Alice", Email: "alice@example.com", Age: 28})
fmt.Println(res.InsertedID)

// Insert many
bulk, err := coll.InsertMany(ctx, []any{
    User{Name: "Bob", Email: "bob@example.com", Age: 32},
    User{Name: "Carol", Email: "carol@example.com", Age: 24},
})

Find

// Find one
var user User
err := coll.FindOne(ctx, nosql.Filter{"email": "alice@example.com"}, &user)

// Find by ID
err = coll.FindByID(ctx, objectID, &user)

// Find multiple with options
var users []User
err = coll.Find(ctx, nosql.Filter{"age": nosql.Document{"$gte": 21}}, &users, nosql.FindOption{
    Sort:  nosql.Sort{"name": nosql.Ascending},
    Limit: 50,
    Skip:  0,
})

// Count & Exists
count, _ := coll.Count(ctx, nosql.Filter{"active": true})
exists, _ := coll.Exists(ctx, nosql.Filter{"email": "alice@example.com"})

Update

// Update one (auto-wraps in $set)
res, err := coll.UpdateOne(ctx,
    nosql.Filter{"email": "alice@example.com"},
    nosql.Filter{"age": 29},
)

// Update many
res, err = coll.UpdateMany(ctx,
    nosql.Filter{"active": false},
    nosql.Filter{"status": "archived"},
)

// Update by ID
res, err = coll.UpdateByID(ctx, objectID, nosql.Filter{"name": "Alice W."})

// Upsert (insert-or-update)
res, err = coll.Upsert(ctx,
    nosql.Filter{"email": "new@example.com"},
    nosql.Filter{"name": "New User", "email": "new@example.com"},
)

// Use raw MongoDB operators
res, err = coll.UpdateOne(ctx,
    nosql.Filter{"_id": id},
    nosql.Filter{"$inc": nosql.Document{"login_count": 1}},
)

Delete

// Delete one
res, err := coll.DeleteOne(ctx, nosql.Filter{"email": "alice@example.com"})

// Delete many
res, err = coll.DeleteMany(ctx, nosql.Filter{"status": "archived"})

// Delete by ID
res, err = coll.DeleteByID(ctx, objectID)

Aggregation

// Aggregation pipeline
pipeline := bson.A{
    bson.D{{Key: "$match", Value: bson.D{{Key: "status", Value: "active"}}}},
    bson.D{{Key: "$group", Value: bson.D{
        {Key: "_id", Value: "$country"},
        {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}},
    }}},
}
var results []bson.M
err := coll.Aggregate(ctx, pipeline, &results)

// Distinct values
emails, err := coll.Distinct(ctx, "email", nosql.Filter{"active": true})

Indexing

// Simple index
name, err := coll.CreateIndex(ctx, nosql.Document{"email": 1})

// Unique index
name, err = coll.CreateIndex(ctx, nosql.Document{"email": 1}, nosql.IndexOption{
    Unique: true,
    Name:   "idx_users_email",
})

// Compound index
name, err = coll.CreateIndex(ctx, nosql.Document{"status": 1, "created_at": -1})

// TTL index (expire docs after 24h)
ttl := int32(86400)
name, err = coll.CreateIndex(ctx, nosql.Document{"expires_at": 1}, nosql.IndexOption{
    ExpireAfterSeconds: &ttl,
})

// Sparse index
name, err = coll.CreateIndex(ctx, nosql.Document{"phone": 1}, nosql.IndexOption{
    Sparse: true,
})

// Drop an index
err = coll.DropIndex(ctx, "idx_users_email")

Models

Embed nosql.Model for standard fields โ€” the same pattern as database.Model on the SQL side:

Base Model

// nosql.Model provides the same standard fields as database.Model (SQL)
type Model struct {
    ID        string      // document _id
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt *time.Time  // soft delete (nil = not deleted)
}

Defining a Model

type Post struct {
    nosql.Model
    Title   string
    Body    string
    Status  string
}

type Comment struct {
    nosql.Model
    PostID  string
    Content string
}

This mirrors the SQL side where models embed database.Model:

SQL (database.Model)NoSQL (nosql.Model)
ID uintID string (MongoDB ObjectID)
CreatedAt time.TimeCreatedAt time.Time
UpdatedAt time.TimeUpdatedAt time.Time
DeletedAt gorm.DeletedAtDeletedAt *time.Time

Validators

Validation works exactly the same as the SQL side โ€” define a validator struct with Rules() and call Validate(). Place validators in app/validators/:

package validators

import "github.com/CodeSyncr/nimbus/validation"

type Post struct {
    Title   string
    Body    string
    Status  string
}

func (v *Post) Rules() validation.Schema {
    return validation.Schema{
        "title":  validation.String().Required().Min(1).Max(255).Trim(),
        "body":   validation.String().Required().Min(1).Max(5000),
        "status": validation.String().In("draft", "published", "archived"),
    }
}

func (v *Post) Validate() error {
    return validation.ValidateStruct(v)
}

Use the validator in your controller โ€” same pattern as CRUD with SQL:

func (pc *PostController) Store(ctx *http.Context) error {
    _ = ctx.Request.ParseForm()

    v := &validators.Post{
        Title:  strings.TrimSpace(ctx.Request.FormValue("title")),
        Body:   ctx.Request.FormValue("body"),
        Status: ctx.Request.FormValue("status"),
    }
    if err := v.Validate(); err != nil {
        return ctx.JSON(http.StatusUnprocessableEntity, map[string]any{"errors": err})
    }

    post := &models.Post{Title: v.Title, Body: v.Body, Status: v.Status}
    _, err := pc.Store.Collection("posts").InsertOne(ctx.Request.Context(), post)
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
    }
    return ctx.JSON(post)
}

Connection Lifecycle

// Health check
if err := nosql.Connection("mongo").Ping(ctx); err != nil {
    log.Println("MongoDB is unreachable:", err)
}

// Close the default *nimbus.NoSQL handle
defer nimbus.CloseNoSQL(ctx)

// Or close all registered NoSQL connections
defer nosql.CloseAll(ctx)

Accessing the Raw Client

For features not exposed by the Collection interface you can reach the underlying *mongo.Client:

driver := nosql.MustConnection("mongo").(*nosql.MongoDriver)
client := driver.Client()   // *mongo.Client
db     := driver.DB()       // *mongo.Database

Next: NoSQL Query Builder โ†’