Skip to content
carameldocs · 0.16.0
Browse documentation
Reference / Routes and contracts

Every input, declared.

Map each method and path to one action, and declare the typed fields that action accepts.

ContractValue
Release0.15.0
NeedsA Caramel application with config/routes.cr
CoversRoutes, request matching, typed contracts and path helpers
Proved byfrappe routes · frappe check

Learning it for the first time? The recipe Create a resource generates the routes and contracts this page describes. This page lists every rule and limit.

Limits at a glance

LimitValue
Path segments32
Times one parameter name may appear in a path1
Values per request4,096
Files per request128
Body text read by default2 MiB
Upload budget shared by files64 MiB
Multipart framing beyond text and uploads1 MiB

Routes live in one draw block

All routes live in one Caramel::Router.draw block in config/routes.cr. Each line names a verb, a path and an action. The verbs are get, post, put, patch and delete.

config/routes.cr · after frappe make resource Book
module App
  Caramel::Router.draw do
    get "/", App::Home::Show
    get "/books", App::Books::Index
    get "/books/new", App::Books::New
    post "/books", App::Books::Create
    get "/books/:id", App::Books::Show
    get "/books/:id/edit", App::Books::Edit
    patch "/books/:id", App::Books::Update
    delete "/books/:id", App::Books::Destroy
    # Frappé resource routes
  end
end
Path partRule
StartBegins with /
Static segmentLetters, digits and ._~-
Parameter:snake_case, such as :id; each name appears once per path, and a path may carry several, such as /teams/:team_id/members/:id
LengthAt most 32 segments

The compiler checks the route table

The route table is checked when the application compiles.

ConditionOutcome
A line that is not one of the five verbs with a string path and an action constant, such as resources :booksfails compilation
A malformed path, a repeated parameter or more than 32 segmentsfails compilation
An action that is undefined, does not inherit from Caramel::Action, or has no contract do … end block (an empty one is fine)fails compilation
A parameter without a contract field of the same namefails compilation
A parameter field that is not a non-nilable String, Int32 or Int64, or a field value bound from one, without default:fails compilation
Two routes that match the same requests (DUPLICATE ROUTE)fails compilation
A static route declared after a parameter route that would catch it (AMBIGUOUS ROUTE ORDER)fails compilation
One tenant App::Model, by: :field do … end block, with caramel/tenancy requiredgroups the routes inside it under the tenant’s address
A second tenant block, one without caramel/tenancy, a route outside it that starts with a parameter, or a route inside it whose first segment a central route also usesfails compilation; Multi-tenancy shows the block

Declare /books/new before /books/:id. A missing field names the route, the contract block and the fix:

Compiler output · excerpt, paths shortened
❌ ROUTE CONTRACT MISMATCH
Route: '/books/:book_id' defines parameter ':book_id'
Action: 'App::Books::Show::Contract' is missing 'field book_id : Type'
  --> config/routes.cr:9:5
Contract: app/actions/books/show.cr:3:5
Remediation: add `field book_id : Int64` to the contract block of App::Books::Show.

Static segments win over parameters

Static segments win over parameters, and matching backtracks. GET /books/new reaches New, and GET /teams/new/members can still reach /teams/:team_id/members. Empty segments never match a parameter, so /books/ is not /books.

ConditionOutcome
No route at the pathanswers 404 with Not found
A route at the path with another methodanswers 405 with Method not allowed and an Allow header, such as GET, HEAD, PATCH
A HEAD request to a GET routeruns it, drops the body, keeps the headers and sets Content-Length
A HEAD request to a streamed responseomits Content-Length
A route parameter that fails its field (field id : Int64, min: 1: /books/abc, /books/0)answers 404
A path over 32 segmentsanswers 404
A malformed path escape, an escape that decodes to /, \ or a control byte, or a raw backslash or control byte in the pathanswers 400 with Malformed path
A route parameter whose decoded bytes are not UTF-8answers 400 with Malformed request

HTML forms can only send GET and POST.

ConditionOutcome
A POST form whose _method is PATCH, PUT or DELETE, in any caseroutes as that method
Any other _method valueanswers 400
A target route that reads its body differently, for example through its own ingressanswers 405
A JSON body with _methoddoes not override; send the real method

Every action declares a contract

Every action declares the fields it accepts. A field is a name, a type and optional bounds. handle runs only when every field is valid, and reads them as typed getters.

app/actions/books/create.cr · contract excerpt
contract do
  field title : String, min: 1, max: 200
  field pages : Int32?, min: 1
  field price : Float64, min: 0
  field published : Bool, default: false
  field released_at : Time?
  field cover : Caramel::UploadedFile?
end
TypeAccepts
StringText as sent; Caramel does not trim it
Int32, Int64Digits with an optional sign
Float64A finite decimal, with an optional exponent
BoolExactly true or false
TimeRFC 3339 with seconds and a zone, such as 2026-09-30T08:00:00Z; read as UTC
Caramel::UploadedFileA multipart file part: filename, content_type, path and size
Array(T)Repeated keys in a form or query (ids=1&ids=2), or a JSON array, of String, Int32, Int64, Float64, Bool, Time or a field value
A field valueA type that includes Caramel::FieldValue(W), such as a SweetTalk value or an aggregate’s id: read as W, then built by its from_field, whose message is the field’s error
ConditionOutcome
? on a fieldgetter returns nil when absent
default:fills an absent field
min: or max: on a Stringbound its length in characters
min: or max: on Int32, Int64 or Float64bound its value
Bounds on other types, defaults on files, any other typefails compilation
min: or max: on a field value (its from_field checks its own bounds)fails compilation
An absent value, or text that is only whitespacecounts as missing
A missing field with a defaulttakes the default
A missing nilable fieldstays nil
A missing field with neitheranswers 422 with is required under the field

An unchecked checkbox sends nothing, so give its Bool field default: false.

ConditionOutcome
An Array(T) field without max:fails compilation
An Array(T) field with ? or a defaultfails compilation
min: or max: on an Array that is not an integer literal, or with min: below 0, max: below 1 or min: above max:fails compilation
min: and max: on an Arraycount items
An absent arrayis empty
A blank itemanswers 422 with is required under name[index]
An item equal to an earlier oneanswers 422 with repeats an earlier item under name[index]
Itemskeep their order
Each itemconverts like a field of its type

Each value binds from one source

A field binds by name from the route parameters, the body or the query string. The body is a URL-encoded form, a multipart form or a JSON object.

ConditionOutcome
A name sent twice, in one source or across twoanswers 422 with Duplicate field: name under _base
The same name for an Array fieldbinds every item
_csrf or _method in a formis a transport control, never a field
A submitted key the contract does not declareanswers 422 with Unknown field: name under _base
An undeclared key in the query of a GET or HEAD requestis ignored, so links can carry tracking or cache-busting parameters
An undeclared query key on other methodsanswers 422 with Unknown field: name under _base
A JSON member of the wrong JSON type (a string for String and Time, a number for the numeric types, a boolean for Bool, an array for Array(T))answers 422 with must be a JSON string, number, boolean or array under the field
null in JSONcounts as absent
A JSON number such as 3.5 for an Int32 (numbers keep their source text)answers 422 with must be a valid Int32 under the field
A JSON body that is not an objectanswers 422 with Expected a JSON object under _base
_csrf and _method in JSONare ordinary members; send X-CSRF-Token and the real method
Broken encoding or JSONanswers 400 Malformed request
A body past the route’s limit, more than 4,096 values (every query and form pair, JSON member, JSON array item and multipart part counts as one), more than 128 file parts, or a multipart body that spends more than 1 MiB on boundaries and part headers beyond its text and uploadsanswers 413 Request body is too large
Any other content typeanswers 415
An ingress authenticator that answers falseanswers 401 before the contract binds

The default limit is 2 MiB of body text; files share a 64 MiB budget. An action’s ingress declaration changes the limit, keeps raw bytes, or adds an authenticator.

Read raw bodies with ingress →Send and receive JSON →

Contract errors collect per field

Errors collect per field name. Errors about the request as a whole go under _base. These are the English messages:

MessageWhen it appears
is requiredA missing field with no default that is not nilable
must be a valid Int32Text that does not convert; also Int64, Float64, Bool and Time
must be a JSON stringA JSON member of the wrong type; also number, boolean and, for an Array field, array
must be at least 3 charactersA String under min:, or over max: with must be at most
must be at least 1A number under min:, or over max: with must be at most
must be a fileText sent for an UploadedFile field
must have at most 100 item(s)An Array over max:, or under min: with must have at least
repeats an earlier itemAn item equal to an earlier one, reported under its key, such as ids[2]
Duplicate field: titleUnder _base: a name sent more than once
Unknown field: extraUnder _base: a key the contract does not declare
Expected a JSON objectUnder _base: a JSON body that is an array or a scalar

An item’s errors use the key name[index], counting from 0, such as ids[1].

ConditionOutcome
A failed contractanswers 422, shaped for the client

A client that prefers JSON receives {"errors": {"page": ["must be a valid Int32"]}}. Other clients receive plain text:

Response body · GET /books?page=2&page=3
ERR CONTRACT_INVALID:422 at GET /books
FIELD _base: Duplicate field: page

With caramel/i18n, these messages follow the request’s language.

Render failures and results →

Path helpers build links

Caramel.resource_paths in config/paths.cr defines four helpers. App::Paths is included in ApplicationAction and ApplicationView, so actions and views can call them.

config/paths.cr
module App::Paths
  Caramel.resource_paths :books, :book
  # Frappé resource paths
end
books_path/books
book_path(id : Int64)/books/12
new_book_path/books/new
edit_book_path(id : Int64)/books/12/edit
ConditionOutcome
A name given to resource_paths that is not a lowercase identifier ([a-z][a-z0-9_]*)fails compilation
An ID that is not positiveraises ArgumentError
Locale prefixes enabledeach helper adds the request’s prefix
caramel/tenancy requiredeach helper adds the request tenant’s prefix; Multi-tenancy shows the addresses

Prove it

Inspect and repair routes

These commands assume the Book resource of Your first application, step 02; generate it first. frappe routes lists every route with its action and contract. A filter keeps routes whose method, path or action contains it, in any case. A field value shows its type and the type it binds from, such as quantity:Ordering::Quantity(Int32). Routes with their own ingress end with a bracketed summary, such as [raw, 256 KiB, csrf off, authenticate signed?].

Terminal · inside the application
frappe routes books

Expect: Prints one line per route whose method, path or action contains books, in any case, seven for a generated Book: method, path, action, then the contract fields. The show route’s line reads GET /books/:id App::Books::Show id:Int64(min=1).

frappe check runs the type check and prints MRDP unless stdout is a terminal. A route without its field becomes a CONTRACT_MISMATCH at the contract block, with a PATCH line when contract do ends its line. Inserting that line makes the check pass. A field of the wrong type gets a FIX line instead.

Terminal · inside the application
frappe check

Expect: Exits 0 and prints OK check N files when piped, ✓ Type check passed on a terminal. A route without its field becomes a CONTRACT_MISMATCH at the contract block, with a PATCH line; inserting that line makes the check pass.

Output · frappe check, a route without its field
ERR CONTRACT_MISMATCH:422 at app/actions/shelves/show.cr:3:5
NODE: RequestContract
MISSING: id:Int64
PATCH: INSERT "field id : Int64" AT 4:7

Passing: frappe check exits 0 once every route parameter has its contract field, and frappe routes books lists each Book route with its contract fields.