Project data · Checked project-wide namespaces
Project contracts.
Project contracts declare named JSON sources in .nift/config.json. They are available project-wide through the existing $[...] value syntax, and each output that references a contract automatically depends on both the contract source and its config declaration. They are the generic mechanism underneath patterns such as route contracts.
Use @json(...) when a template explicitly loads page/render data. Use a project contract when the same named relationship should mean the same thing everywhere in the project.
Declare contracts in config.json
{
"config": {
"content-dir": "content/",
"output-dir": "public/",
"contracts": {
"routes": ".nift/routes.json",
"services": "data/services.json"
}
}
} Each key is a contract namespace. Each value is a non-empty project-relative path to a JSON file. Contract names use identifier-style letters, digits and underscores and cannot collide with Nift's built-in/reserved namespaces.
Read contract values with $[...]
Given:
// .nift/routes.json
{
"users": {
"list": "/api/users",
"details": "/api/users/{id}"
}
} a template can use:
const response = await fetch('$[routes.users.list]'); Object members and array indices use the same access rules as @json bindings. Scalar strings, numbers, booleans and null render as values; whole objects and arrays must be narrowed to a member/element before rendering.
Resolution is explicit, not guessed
$[routes.users.list] has contract meaning only when routes is declared in config. Nift does not search the filesystem for arbitrary unknown namespaces and does not assume a typo is a missing contract.
If no contract named routes exists, a local @json(..., routes) binding can still use that name normally. If neither exists, the expression follows ordinary unknown-value behavior.
Contract namespaces cannot be shadowed
Once a contract name is declared, that name has one meaning project-wide. Nift rejects attempts to reuse it as a local JSON binding or loop binding:
@json('data/other.json', routes) // error if routes is a contract
@for(routes : data.items){ ... } // error if routes is a contract Nift also rejects contract names that collide with built-in metadata or reserved bindings. There are no precedence rules to memorize.
Contract sources are loaded lazily
Declaring ten contracts does not make every page depend on all ten files. Nift loads a contract when that namespace is actually resolved during a render and caches the parsed immutable JSON for the build.
page A uses $[routes.*] → depends on config.json + routes source
page B uses $[services.*] → depends on config.json + services source
page C uses neither → no contract-source dependency Incremental builds follow contract changes
When a page references a contract, Nift records the contract file and .nift/config.json in the page's dependency metadata. Therefore both of these can invalidate the output:
- editing the referenced contract JSON;
- changing the config mapping so the namespace points at a different contract file.
After a successful rebuild, the persisted dependency set is refreshed to the new source.
Failures are contract-specific
Declared contract namespaces produce controlled errors for missing sources, malformed JSON, missing referenced members, unsafe paths and non-renderable object/array values. Config itself rejects malformed contract declarations before rendering starts.
contract 'routes': file does not exist: .nift/routes.json
contract 'routes' has no entry 'users.details' Use JSON Schema when the contract needs a richer shape guarantee
Project contracts currently provide named existence/member/dependency guarantees over JSON. If a project needs richer domain validation, keep the contract data declarative and validate it with appropriate tooling or an explicit JSON Schema workflow. Nift should not pretend to understand domain semantics it cannot actually verify.
Route contracts are one concrete pattern
A shared frontend/backend route catalogue is a natural project contract because route drift is otherwise easy to express as duplicated unchecked strings. The dedicated Routes page shows the pattern and explains how Nift route contracts differ from deployment-platform routing rules such as Vercel routes.