Go REST endpoints#

The Go generator can make a REST API for each top_level struct. The endpoints use the postgres feature, so you must also enable that feature. Enable the endpoints in model.toml:

[tool.model.go]
out = "output.go"
package = "mypkg"

[tool.model.go.features]
postgres = 1

[tool.model.go.rest]
base-path = "/api"

base-path must start with /. The generator removes a trailing /.

Endpoints#

The path segment of a struct is its name in kebab-case. For example, SaveRecord becomes save-record. A struct T with the segment t gets these endpoints:

Request

Operation

Success

POST /api/t

Creates a row from the body.

201 Created with Location, ETag, and the row.

GET /api/t/{id}

Reads the row. HEAD is also served.

200 OK with ETag and the row.

PUT /api/t/{id}

Replaces a row that exists. It does not create a row.

200 OK with the new ETag and the row.

PATCH /api/t/{id}

Applies a JSON Merge Patch (RFC 7386).

200 OK with the new ETag and the row.

DELETE /api/t/{id}

Deletes the row.

204 No Content.

{id} is the value of the struct’s primary key. These conditions apply to the struct:

  • It must have exactly one primary_key field.

  • The key must be a string, an integer, or an enum. An enum key uses the name of the enum value.

If a struct does not meet these conditions, generation fails. Generation also fails in these conditions:

  • Two structs have the same path segment.

  • A model route uses the same method and path as an endpoint.

Register the endpoints on a mux. The handlers use the request transaction, so DBMiddleware must wrap the mux:

mux := http.NewServeMux()
genmodel.RegisterRestEndpoints(mux)
genmodel.RegisterAllEndpoints(mux) // only if the model has routes
return http.ListenAndServe(":8080", genmodel.DBMiddleware(pool)(mux))

Each handler commits or rolls back the transaction before it writes the response. If the commit fails, the client gets status 500.

Bodies#

Request and response bodies are JSON. If you enable the yaml feature, a request with a YAML Content-Type is read as YAML. A request whose Accept header prefers YAML gets a YAML response.

PATCH accepts only application/merge-patch+json or application/json. For other content types, the response is 415 with an Accept-Patch header. The patch is applied to the JSON form of the row. The result is then decoded and validated. A patch cannot change the primary key.

Entity tags and conditional requests#

The ETag is a strong entity tag, computed from the JSON form of the row. A YAML response has a different tag, because the representation is different.

Header

GET / HEAD

PUT / PATCH / DELETE

If-None-Match

304 Not Modified if a tag matches the tag of the response, or for *.

412 if a tag matches the row, or for *.

If-Match

412 if no tag matches the row.

412 if no tag matches the row, or if the row does not exist.

For If-Match, a tag from the JSON or the YAML representation is accepted. Weak tags (W/"...") never match If-Match. A write request locks the row (SELECT ... FOR UPDATE) before it compares the tags, so another request cannot change the row between the comparison and the write.

Errors#

Errors are application/problem+json bodies (RFC 9457) with title, status, and detail. A validation failure also lists each failed constraint in errors, with kind, path, and message.

PUT and PATCH accept a row that fails validation if it adds no violations to the stored row. For the rule, see stored rows that fail validation. If the row adds violations, the response is 422:

  • errors lists only the new violations.

  • existing_errors lists all the violations of the stored row. It has the same form as errors.

Other validation failures do not have existing_errors.

{
    "title": "Unprocessable Entity",
    "status": 422,
    "detail": "the update adds violations the stored value does not have: note: must have no more than 5 characters",
    "errors": [{"kind": "length", "path": "note", "message": "must have no more than 5 characters"}],
    "existing_errors": [{"kind": "range", "path": "id", "message": "must be no less than 1"}]
}

Status

Cause

400

The body or the {id} segment cannot be decoded.

404

The row does not exist.

409

The row already exists (POST), or a foreign key fails.

412

A precondition failed.

415

A PATCH body has an unsupported content type.

422

The row fails Validate() or a database constraint. The body key does not match the path, or a patch changes the key.

500

Any other failure. The detail is logged and not sent, except ErrNoDB, which means that DBMiddleware is missing.