Guide / Reinhardt 0.3.15

Run Reinhardt 0.3.15's REST example: database-backed CRUD in Rust

In this guide

If you already write Rust and want to follow a small API from an HTTP request to a database row, Reinhardt's existing REST tutorial is a useful place to start. Its example manages code snippets: a title, some code, and a language.

This walkthrough runs that example, exercises CRUD and invalid input, and points to the code responsible for each step. It targets Reinhardt 0.3.15 and includes two setup details needed for this version: a local settings file and the management server feature. The goal is to make the first attempt reproducible and learn where developers get stuck.

Prepare a local environment

You need Git, Rust 1.96.0 or later with Cargo, a running Docker engine, Python 3.11 or later for the infrastructure scripts, Bash, and curl. These commands use the supplied scripts directly; cargo-make is not required. Use a normal local shell without CI or REINHARDT_ENV profile overrides.

Keep ports 5432, 6379, and 8000 free. The helper recreates the disposable containers named examples-tutorial-rest-postgres and examples-tutorial-rest-redis; running it again removes data from an earlier run in those containers. Use this as a local, disposable demonstration.

Clone the pinned tag into a fresh directory:

git clone --depth 1 --branch '[email protected]' https://github.com/kent8192/reinhardt-web.git reinhardt-rest-demo
cd reinhardt-rest-demo/examples/examples-tutorial-rest
git rev-parse HEAD

Check that the printed commit is c64a11e2dd945eab35ce3d56d812b0b78ff1b327. Subsequent commands in this terminal run from examples/examples-tutorial-rest.

Supply the local settings and start the API

The tag does not contain settings/local.toml or a matching example template. Create the local settings expected by the infrastructure helper:

cat > settings/local.toml <<'TOML'
[core]
debug = true
allowed_hosts = ["localhost", "127.0.0.1"]
TOML

These are development settings for the local demonstration. Database configuration is inherited from the existing base settings. Start the supplied containers:

bash scripts/infra_up.sh

The helper starts PostgreSQL and Redis. The snippet API stores its data in PostgreSQL; it does not currently use Redis. Redis is started by the shared example infrastructure.

Apply migrations:

cargo run --features reinhardt/commands-server --bin manage -- migrate

Then start the development server:

cargo run --features reinhardt/commands-server --bin manage -- runserver 127.0.0.1:8000 --noreload

The explicit reinhardt/commands-server feature matters in 0.3.15. During the initial check, the default feature selection built and migrated successfully, but runserver printed Server feature not enabled, exited with code 0, and never accepted HTTP requests. The commands above enable the management command's server implementation. Confirm startup with the HTTP request below.

The first Cargo invocation compiles the example and its dependencies. Leave the server running and open a second terminal for requests.

Create and retrieve a snippet

Read the current list:

curl -i http://127.0.0.1:8000/api/snippets/

With the freshly recreated database, the list should be empty. The -i option keeps HTTP status and response headers visible. Create a snippet:

curl -i -X POST http://127.0.0.1:8000/api/snippets/ \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello Reinhardt","code":"fn main() {}","language":"rust"}'

Look for HTTP 201 and the created object's snippet.id. Replace REPLACE_WITH_RETURNED_ID below with that actual integer before executing the assignment. Keep this variable in the second terminal; do not assume the ID is 1.

SNIPPET_ID=REPLACE_WITH_RETURNED_ID

Retrieve that record:

curl -i "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/"

Look for HTTP 200 and the title, code, and language you sent.

Update, delete, and check invalid input

Update the same ID:

curl -i -X PUT "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/" \
  -H 'Content-Type: application/json' \
  -d '{"title":"Updated snippet","code":"fn main() { println!(\"updated\"); }","language":"rust"}'

Check the 200 response for Updated snippet and the updated code. Delete the record:

curl -i -X DELETE "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/"

A successful deletion returns 204 with no response body. Request the same ID again:

curl -i "http://127.0.0.1:8000/api/snippets/${SNIPPET_ID}/"

Replaying this command returned 404 with {"error":"Snippet not found"} after deletion.

Now send valid JSON whose code field is an empty string:

curl -i -X POST http://127.0.0.1:8000/api/snippets/ \
  -H 'Content-Type: application/json' \
  -d '{"title":"Invalid snippet","code":"","language":"rust"}'

This exercises a field constraint rather than malformed JSON. Replaying this command returned 400. Inspect the error body, then confirm that no invalid record was added:

curl -i http://127.0.0.1:8000/api/snippets/

If you only performed the steps above, the final list should be empty.

Follow the request through the source

Start in src/config/urls.rs::routes, which mounts the snippets router under /api/. url_patterns registers the function-based list, create, retrieve, update, and delete handlers.

In views.rs::create, the request body becomes Json<SnippetSerializer> and the database connection arrives through #[inject]. The route declares validation before the handler body runs:

#[post("/snippets/", name = "snippets-create", pre_validate = true)]

The handler builds a Snippet from the accepted fields. Its manager performs the database write using that connection:

let created = Manager::<Snippet>::new()
    .create_with_conn(&db, &snippet)
    .await?;

SnippetSerializer defines the input length constraints, including the nonempty code requirement. SnippetResponse::from_model selects the response fields before the handler serializes the JSON response.

For comparison, views.rs::update calls serializer.validate()? explicitly. Create and update therefore use different validation paths in this version; the empty-code result demonstrated here is for POST.

What was checked

On 2026-09-05, a fresh clone matched the pinned commit. The settings heredoc and infrastructure script printed above ran successfully. The exact Cargo commands applied three migrations and started an HTTP listener. The eight curl requests were then executed from this article's command blocks, substituting the actual returned ID.

Printed curl requestObserved result
Initial list200
Create201; title, code, and language matched
Retrieve created ID200; fields matched
Update200; updated fields matched
Delete204; empty body
Retrieve deleted ID404; {"error":"Snippet not found"}
Create with empty code400
Final list200; {"snippets":[]}

Separately, the initial check ran the existing Bruno Snippets CRUD and Validation Tests collections: 8 requests and 12 tests passed. Its additional missing-title POST also returned 400; that request is not part of the eight curl requests above.

These observations cover the native function-based REST example. This walkthrough does not validate every framework feature. Pages is under development, and production use is not yet recommended; Pages testing will have a separate scope.

Stop the demonstration and tell us where it hurt

Stop the server with Ctrl-C in the first terminal, then run this from the same example directory:

bash scripts/infra_down.sh

This stops the disposable infrastructure and removes its data.

Try the example on your own machine, then change one thing at a time. For a usage question, open GitHub Discussions. For a reproducible defect, use the Issue form and include the version or commit, OS and Rust version, exact command, expected result, and actual result. A specific setup failure is useful feedback: it tells us which step needs improvement.

Disclosure: This article was drafted with LLM assistance and checked against the pinned source and the execution results described above.