tutorials

Save and read your first data

Build a small notes API backed by the document store, with no schema migration to manage.

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 /notes saves a note.
  • GET /notes/:id reads 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 init and marreta serve are 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

bash
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:

bash
docker compose up -d --wait

2. 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:

marreta
export schema NewNote
    title: string
    body: string

3. 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:

marreta
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:

marreta
route GET "/notes/:id"
    note = doc.notes.find(params.id)
    require note else fail 404, "note not found"
    reply 200, note

5. 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.

marreta
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 200
bash
marreta test

6. Run it

Start the server:

bash
marreta serve

In another terminal, create a note and read it back using the returned id:

bash
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