CRUD, bcrypt-hashed passwords and real aggregation pipelines over an Atlas cluster — from config. No ORM, no Mongoose, no server to deploy.
If you already have one, skip to step 2. Otherwise:
| Name | Value | |---|---| | MONGODB_URI | the SRV string from step 1 |
Declare the database once, then each interface is a route:
Those assert tests aren't only guards — they generate the OpenAPI request body. The description: on each becomes the field description in the published schema, so the docs stay correct because they're the same declaration that does the validating. There's no second place to update.
This is where hand-rolled tutorials usually hand-wave. Hashing happens as a transform between validation and insert:
Everything so far is CRUD, and CRUD looks the same on any database. This part doesn't.
Air Pipe validates JWTs; it doesn't mint them. Your app already issues tokens, so validation is the first action and everything else waits on it:
From the same files, no extra work: an OpenAPI document generated from the assert tests and response_example blocks, Prometheus metrics per route, and an OpenTelemetry trace per request showing each action and how long the Mongo call took. When an aggregation gets slow, the trace tells you whether it's the pipeline or the network before you start guessing.
All of it ships as one pack — users, posts and categories CRUD, the four aggregation endpoints, the JWT pattern, and a seed endpoint that loads sample data so the analytics have something to chew on. Set MONGODB_URI, deploy, call /api/seed.
$set overwrites. MongoDB update semantics replace the fields you send rather than merging, so the update endpoints require the full set of fields you want changed. There's no COALESCE-style partial update. No reference integrity. A post's author and category are plain strings; Mongo won't check they point at a real user or category. That's the trade for schema flexibility, and keeping them consistent is your application's job. Human keys, not ObjectIds. Lookups use username and slug rather than _id, which keeps URLs readable and avoids ObjectId casting on every request. Both need a unique index in Atlas — add one, because nothing else will stop a duplicate.