Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/tutosrive/ferreandina-nosql/llms.txt

Use this file to discover all available pages before exploring further.

Every resource in the Ferreandina API — branches, products, suppliers, customers, workers, and more — is served by a concrete class that extends the abstract Controller<T extends Model>. Controller<T> implements Javalin’s CrudHandler interface and provides create, getAll, getOne, update, and delete out of the box. Adding a new resource requires nothing more than a three-line subclass that names the collection. Custom aggregation or filter endpoints can be layered on top as ordinary methods.

Controller Base Class

Controller<T> holds references to a Validator<T>, a ResultUtil<T>, and a Service<T>. The constructor wires all three together and binds the service to the named MongoDB collection.
package com.ferreandina.controllers;

import java.util.Map;
import org.bson.conversions.Bson;
import com.ferreandina.models.Model;
import com.ferreandina.services.Service;
import com.ferreandina.utils.ResultUtil;
import com.ferreandina.utils.Utils;
import com.ferreandina.utils.Validator;
import com.mongodb.client.FindIterable;
import com.mongodb.client.model.Filters;

import io.javalin.apibuilder.CrudHandler;
import io.javalin.http.Context;

public abstract class Controller<T extends Model> implements CrudHandler {
    protected Validator<T> validator;
    protected ResultUtil<T> resultMan;
    protected Service<T> service;
    protected Class<T> clazz;

    public Controller(Class<T> clazz, String collectionName) {
        this.clazz = clazz;
        this.validator = new Validator<T>();
        this.resultMan = new ResultUtil<>();
        this.service = new Service<T>(this.clazz);
        this.service.setCollection(collectionName);
    }

    @Override
    public void create(Context ctx) {
        T document = this.validator.validateBody(this.clazz, ctx);
        Integer insertedId = this.service.add(document).asInt32().getValue();
        this.resultMan.javalinReturn(ctx,
                String.format("%s inserted with ID '%d'", this.clazz.getSimpleName(), insertedId));
    }

    @Override
    public void delete(Context ctx, String id) {
        Integer idInt = Integer.parseInt(id);
        Bson filter = Filters.eq("_id", idInt);
        Long deletedCount = this.service.delete(filter);
        this.resultMan.javalinUpdateDelete(
                ctx, deletedCount, true,
                String.format("%s deleted with id: %d", this.clazz.getSimpleName(), idInt));
    }

    @Override
    public void getAll(Context ctx) {
        FindIterable<T> data = this.service.getAll();
        this.resultMan.javalinReturn(ctx, data, String.format("There are all %ss", this.clazz.getSimpleName()));
    }

    @Override
    public void getOne(Context ctx, String id) {
        Integer idInt = Integer.parseInt(id);
        Bson filter = Filters.eq("_id", idInt);
        T document = this.service.getOne(filter);
        this.resultMan.javalinReturn(
                ctx, document, String.format("%s get with id: %s", this.clazz.getSimpleName(), id));
    }

    @Override
    public void update(Context ctx, String id) {
        try {
            Integer idInt = Integer.parseInt(id);
            Bson filter = Filters.eq("_id", idInt);
            @SuppressWarnings("unchecked")
            Map<String, Object> bodyMap = ctx.bodyAsClass(Map.class);
            Bson updates = Utils.fromMapToBsonUpdate(bodyMap);
            Long updatedCount = this.service.updateOne(filter, updates);
            this.resultMan.javalinUpdateDelete(
                    ctx, updatedCount, false,
                    String.format("%s with id %d updated", this.clazz.getSimpleName(), idInt));
        } catch (Exception e) {
            this.resultMan.javalinReturn(ctx, e);
        }
    }
}

Method breakdown

MethodDescription
create(Context ctx)Parses and validates the request body into a T instance via Validator. Calls service.add() and returns the inserted integer _id.
getAll(Context ctx)Calls service.getAll(), which performs a bare collection.find(). Returns every document in the collection wrapped in the standard JSON envelope.
getOne(Context ctx, String id)Parses id as an integer, builds a Filters.eq("_id", idInt) filter, and returns the first matching document.
update(Context ctx, String id)Reads the request body as a raw Map<String, Object> and converts it to a Bson $set update via Utils.fromMapToBsonUpdate(). List values are converted to $push with $each. Errors are caught and returned as a structured JSON error object.
delete(Context ctx, String id)Deletes the document whose integer _id matches. Returns a delete_count field in the response data.

Creating a Resource Controller

Any resource that needs only standard CRUD is a three-line class. Pass the model class and the MongoDB collection name to the parent constructor — everything else is inherited.
package com.ferreandina.controllers;

import com.ferreandina.models.CustomerModel;

public class CustomerController extends Controller<CustomerModel> {
    public CustomerController() {
        super(CustomerModel.class, "customers");
    }
}
This single class provides POST /api/customers, GET /api/customers, GET /api/customers/{id}, PATCH /api/customers/{id}, and DELETE /api/customers/{id} with no additional code.

Custom Query Methods

When a resource requires queries beyond standard CRUD — such as aggregation pipelines or array sub-document operations — you add them as ordinary public methods on the subclass. BranchController demonstrates this pattern with four extra endpoints. The getLowStockProducts method runs a MongoDB aggregation pipeline that unwinds the embedded products array, filters for items with quantity < 10, and projects a trimmed result:
package com.ferreandina.controllers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.bson.Document;
import org.bson.conversions.Bson;
import org.json.JSONArray;

import com.ferreandina.models.BranchModel;
import com.mongodb.client.model.Aggregates;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.Updates;
import com.mongodb.client.result.UpdateResult;

import io.javalin.http.Context;

public class BranchController extends Controller<BranchModel> {
    public BranchController() {
        super(BranchModel.class, "branches");
    }

    // GET /branches/low-stock
    public void getLowStockProducts(Context ctx) {
        try {
            List<Bson> pipeline = Arrays.asList(
                    Aggregates.unwind("$products"),
                    Aggregates.match(Filters.lt("products.quantity", 10)),
                    Aggregates.project(Projections.fields(
                            Projections.excludeId(),
                            Projections.computed("sucursal", "$name"),
                            Projections.computed("producto", "$products.name"),
                            Projections.computed("stock", "$products.quantity"))));

            List<Document> result = this.service.getConn().collection
                    .withDocumentClass(Document.class)
                    .aggregate(pipeline)
                    .into(new ArrayList<>());

            JSONArray data = new JSONArray().put(result);

            this.resultMan.javalinReturn(ctx, data, "Productos con bajo stock obtenidos");
        } catch (Exception e) {
            this.resultMan.javalinReturn(ctx, e);
        }
    }

    // ... getBranchProducts, removeProductFromBranch, cleanOutOfStock
}
Custom methods access the underlying MongoCollection directly via this.service.getConn().collection, giving full access to the MongoDB Java driver API. The withDocumentClass(Document.class) call overrides the typed collection to return raw BSON Document objects, which is useful when the aggregation output shape differs from the model class.

Service Layer

Service<T extends Model> sits between controllers and the database. It wraps a Connection<T> and exposes a set of final convenience methods over the MongoDB Java driver.
package com.ferreandina.services;

import org.bson.BsonValue;
import org.bson.conversions.Bson;

import com.ferreandina.database.Connection;
import com.ferreandina.models.Model;
import com.mongodb.client.FindIterable;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.InsertOneResult;
import com.mongodb.client.result.UpdateResult;

public class Service<T extends Model> {
    protected Connection<T> conn;
    private String collectionName;

    public Service(Class<T> clazz) {
        this.conn = new Connection<T>(clazz);
    }

    public final BsonValue add(T document) {
        InsertOneResult result = this.conn.collection.insertOne(document);
        return result.getInsertedId();
    }

    public final FindIterable<T> getAll() {
        return this.conn.collection.find();
    }

    public final T getOne(Bson filter) {
        T document = this.getByAggregate(filter).first();
        return document;
    }

    public final FindIterable<T> getByAggregate(Bson filters) {
        return this.conn.collection.find(filters);
    }

    public final Long updateOne(Bson query, Bson update) {
        UpdateResult result = this.conn.collection.updateOne(query, update);
        return result.getModifiedCount();
    }

    public final Long updateMany(Bson query, Bson update) {
        UpdateResult result = this.conn.collection.updateMany(query, update);
        return result.getModifiedCount();
    }

    public final Long delete(Bson query) {
        DeleteResult result = this.conn.collection.deleteOne(query);
        return result.getDeletedCount();
    }

    public Connection<T> getConn() {
        return this.conn;
    }

    public void setCollection(String collectionName) {
        this.collectionName = collectionName;
        this.conn.collection = this.conn.getCollection(this.collectionName);
    }
}
All methods are final to prevent accidental overriding in subclasses. Controllers that need raw driver access can call service.getConn() to retrieve the Connection<T> and work with collection directly.

Utilities

ResultUtil<T>

A uniform JSON response wrapper. Normal responses use the envelope { message, status, data, size }. Dedicated overloads handle FindIterable<T>, single documents, plain strings, and update/delete counts. On error, javalinReturn(ctx, Exception e) sets HTTP status 500 and returns { message, data, error, status } where error contains message, cause, class, and stack-trace fields.

Validator<T>

Thin wrapper around Javalin’s built-in body validator. validateBody(Class<T>, Context) calls ctx.bodyValidator(clazz) with a non-null check and calls .get() to deserialize and validate in one step. Javalin automatically returns a 400 Bad Request if validation fails.

Utils

Converts a Map<String, Object> into a Bson update document suitable for updateOne. For each entry: scalar values become Updates.set(key, value) operations; List values become Updates.pushEach(key, list) operations. Fields named id or _id are skipped. All individual updates are combined with Updates.combine().

Route Registration

Routes.java receives the JavalinConfig from App.java and registers all endpoints inside a single apiBuilder block. The crud() helper registers five standard endpoints for a path in one call: GET, GET /{id}, POST, PATCH /{id}, and DELETE /{id}. Custom endpoints are added as individual get() or patch() calls before the crud() block so they are not shadowed by the wildcard /{id} routes.
package com.ferreandina.routes;

import com.ferreandina.controllers.*;
import io.javalin.config.JavalinConfig;
import static io.javalin.apibuilder.ApiBuilder.*;

public class Routes {
    public Routes(JavalinConfig config) {
        BranchController branchController = new BranchController();
        CategoryController categoryController = new CategoryController();
        ProductController productController = new ProductController();
        SupplieController supplyController = new SupplieController();
        SupplierController supplierController = new SupplierController();
        CustomerController customerController = new CustomerController();
        WorkerController workerController = new WorkerController();

        config.routes.apiBuilder(() -> {
            path("/api", () -> {
                get("/branches/low-stock", branchController::getLowStockProducts);
                patch("/branches/clean-out-of-stock", branchController::cleanOutOfStock);
                get("/branches/{id}/products", branchController::getBranchProducts);
                patch("/branches/{id}/remove-product/{productId}", branchController::removeProductFromBranch);

                // Products
                get("/products/category/{categoryId}", productController::getProductsByCategory);

                // Supplies
                get("/supplies/defective-report", supplyController::getDefectiveReport);

                crud("/branches/{id}", branchController);
                crud("/categories/{id}", categoryController);
                crud("/products/{id}", productController);
                crud("/supplies/{id}", supplyController);
                crud("/suppliers/{id}", supplierController);

                crud("/customers/{id}", customerController);
                crud("/workers/{id}", workerController);
            });
        });
    }
}
Each call to crud("/resource/{id}", controller) maps the five standard HTTP verbs to the corresponding methods on the controller. Custom method references (e.g., branchController::getLowStockProducts) are registered as standalone routes before their crud() block to ensure they match before the parameterized {id} segment.

Build docs developers (and LLMs) love