The Quickstart returned a greeting from memory. The natural next step is to keep data around. This tutorial builds a small notes API on the document store, which is the simplest way to persist in Marreta: you save a map and read it back, with no table to define and no migration to run.
You will build two endpoints:
POST /notessaves a note.GET /notes/:idreads one back.
When you are ready for a relational database with typed columns and versioned schema changes, see Build a relational API with migrations. This page is the gentler starting point.
Prerequisites
- The Quickstart finished, so
marreta initandmarreta serveare familiar. - Docker with Compose, to run the local document provider (Docker Desktop on macOS or Windows, or Docker Engine on Linux or Windows via WSL).
1. Scaffold a project with a document store
marreta init notes --with doc
cd notes--with doc adds a docker-compose.yml with the document provider and a
marreta.env already pointing at it. Start it and wait until it is ready:
docker compose up -d --wait2. Validate the input
The document store accepts whatever map you save, so there is no table or migration.
You still want a schema for the request body, to reject malformed input. Put this in
schemas/notes.marreta:
export schema NewNote
title: string
body: string3. Write the create route
Create routes/notes.marreta. Validate the body, then save the note. save
returns the stored record, including the _id the document store generates:
route POST "/notes" take payload as NewNote
note = doc.notes.save({
title: payload.title,
body: payload.body
})
reply 201, { id: note._id, title: note.title }doc.notes names the collection. It does not need to be declared anywhere. The
first save creates it.
4. Write the read route
Add this to the same routes/notes.marreta: a route to read one note back by its
_id. If there is none, fail with a 404:
route GET "/notes/:id"
note = doc.notes.find(params.id)
require note else fail 404, "note not found"
reply 200, note5. Test it
A scenario test stubs the document calls with given, so it needs no running
provider. Put it in tests/notes_test.marreta: marreta test only runs files under
tests/ whose names end in _test.marreta, so the suffix is required.
scenario "creates a note"
given doc.notes.save(anything) returns {
_id: "note-1",
title: "First note"
}
when POST "/notes" with {
title: "First note",
body: "hello"
}
then status 201
scenario "reads a note back"
given doc.notes.find("note-1") returns {
_id: "note-1",
title: "First note"
}
when GET "/notes/note-1"
then status 200marreta test6. Run it
Start the server:
marreta serveIn another terminal, create a note and read it back using the returned id:
curl -s -X POST http://localhost:8080/notes \
-H 'content-type: application/json' \
-d '{"title":"First","body":"hello"}'
# example output: { "id": "<generated-id>", "title": "First" }
# replace <generated-id> with the id returned above
curl -s http://localhost:8080/notes/<generated-id>
# → { "title": "First", "body": "hello", "_id": "<generated-id>" }The _id is generated by the document store, so the value you get will differ.
Result checkpoint
You should now have a running document provider and two endpoints: one that saves a
note and returns its generated id, and one that reads a note by id or returns a
404. You did all of this with no table definition and no migration.
Next steps
- Build a relational API with migrations: the same idea with a typed, versioned relational schema.
- Validate a request payload: go deeper on request contracts.
docnamespace: the full set of document operations.