This file is the repository knowledge base for collaborators and future AI agents working on Spry.
It should help a new contributor quickly understand:
- what Spry is
- how the repository is organized
- where to make changes
- how to validate work
- how commits, PRs, changelogs, and releases should be written
Spry is a Dart server framework centered on filesystem routing and generated runtime output.
The authoring model is:
- application routes live in
routes/ - global middleware lives in
middleware/ - scoped middleware and error handlers live inside
routes/as_middleware.dartand_error.dart - build/runtime behavior lives in
spry.config.dart - generated runtime output goes to
.spry/by default
Spry targets multiple runtimes:
- Dart VM
- Node.js
- Bun
- Cloudflare Workers
- Vercel
- Netlify Functions
The core pipeline is:
- load config
- scan the project tree
- build a route tree
- generate runtime entry files
- write generated output
- optionally compile JS for non-Dart targets
In practice:
spry serveruns the pipeline and starts a dev/runtime processspry buildruns the pipeline and emits deployable artifacts
Spry is intentionally explicit:
- the filesystem is the source of truth for route structure
- generated output is inspectable
- route behavior should not be hidden behind a large imperative DSL
lib/spry.dartmain framework export surfacelib/app.dartsmaller app-oriented export surfacelib/config.dartdefineSpryConfig(...)and target enumslib/builder.dartbuilder-facing exports for config, scanning, and generation
lib/src/app.dartmainSpryrequest handling pipelinelib/src/routing.dartroute, middleware, and error router constructionlib/src/event.dartrequest-scoped context passed to handlerslib/src/errors.dartHTTPErrorand framework error conversionlib/src/public*.dartstatic asset serving across runtimes
lib/src/builder/config.dartload and merge build configlib/src/builder/scanner.dartscan routes, middleware, errors, and hooks from the project treelib/src/builder/route_tree.dartin-memory metadata model for scanned fileslib/src/builder/generator.dartemit generatedapp.dart,hooks.dart, andmain.dartlib/src/builder/target_spec.darttarget-specific generation details
bin/spry.dartCLI entrypointbin/src/build.dartspry buildbin/src/serve.dartspry servebin/src/build_pipeline.dartshared pipeline orchestrationbin/src/write.dartwrite generated files and sync target assets
README.mdpackage homepage and quick orientationCHANGELOG.mdlong-lived release historysites/spry.medz.dev/documentation sourcesites/spry.medz.dev/snippets/embedded docs snippets
example/dart_vm/example/node/example/bun/example/cloudflare/example/vercel/example/netlify/example/knex_dart/
These examples are useful smoke-test targets when runtime/build behavior changes.
Spry uses filesystem routing.
Common mappings:
routes/index.dart->/routes/about.get.dart->GET /aboutroutes/users/[id].dart->/users/:idroutes/[...slug].dart->/**:slug
Supported expressive segment syntax includes:
- embedded params
- regex params
- optional params
- repeated params
- single-segment wildcards
When changing route parsing behavior, always inspect:
lib/src/builder/scanner.darttest/scanner_test.darttest/generator_test.dart- docs in
sites/spry.medz.dev/guide/routing.md
Spry currently aligns with:
ht 0.3.xosrv 0.6.xroux 0.5.x
Important current expectations:
RequestandResponseuse Fetch-style init objects- manual remainder route strings must use
/**, not/* RequestInitandResponseInitare re-exported by Spry
When changing upstream alignment, verify:
- constructor examples in docs and README
- route syntax docs and examples
CHANGELOG.md- migration docs
Also check:
lib/spry.dartlib/app.dart- examples
- docs snippets
- migration notes
- changelog entry
Also check:
scanner_test.dartgenerator_test.dart- docs routing examples
- generated output assumptions in CLI tests
Also check:
bin/spry.dart- CLI tests
- help text
- README and docs command examples
Also check:
target_spec.dartbuild_pipeline.dart- target-specific examples
- deploy docs under
sites/spry.medz.dev/deploy/
Default validation commands:
dart analyze
dart testWhen docs change:
cd sites/spry.medz.dev
npm run docs:buildWhen runtime/build behavior changes, smoke-test key examples:
cd example/dart_vm && dart pub get && dart run spry build
cd example/node && dart pub get && dart run spry build
cd example/bun && dart pub get && dart run spry build
cd example/cloudflare && dart pub get && dart run spry build
cd example/vercel && dart pub get && dart run spry build
cd example/netlify && dart pub get && dart run spry build
cd example/knex_dart && dart pub get && dart run spry buildWhen preparing a publish:
dart pub publish --dry-run- prefer focused changes over broad refactors
- keep examples and docs aligned with behavior changes
- keep generated/runtime semantics explicit and inspectable
- do not leave release-facing version branding stale
- prefer updating tests and migration docs in the same change as the behavior change
This repository targets sdk: ^3.10.0, so both of the following are available
and should be preferred where they improve clarity:
- Dart 3.8+ collection null-aware elements
- Dart 3.10+ dot shorthands
Rules:
- prefer legal dot shorthand when it is clearer or shorter
- dot shorthand is the official Dart term for forms like
.new(),.running,.parse('80'), and.origin() - dot shorthand requires a clear context type; if the context type is not obvious, keep the explicit form
- prefer null-aware syntax over manual null checks
- in collection literals, prefer Dart null-aware elements over
if (x != null)guards - for nullable map values, prefer forms like
{ 'id': ?id } - for nullable chained calls, prefer forms like
{ 'startsAt': ?startsAt?.toIso8601String() } - for nullable collection transforms, prefer forms like
{ 'items': ?items?.map(...).toList(growable: false) } - prefer
...?itemsfor nullable spread collections - choose the shorter form when dot shorthand is not actually more concise; for
example, prefer
final openapi = OpenAPI(...)overfinal OpenAPI openapi = .new(...) - do not rewrite existing files only to modernize syntax
- new files should follow these rules by default
- when touching existing code, opportunistically upgrade syntax in the edited area
Spry follows semantic versioning, but version planning should also guide implementation behavior.
Major releases may introduce intentional breaking changes.
Rules:
- do not preserve old behavior only for compatibility if the new major version is intentionally redefining the contract
- prefer a clean new model over compatibility shims and legacy branches
- document user-facing breaking changes in migration docs, changelog entries, and release notes
- remove outdated examples and docs instead of preserving contradictory legacy behavior
Minor and patch releases should preserve existing public behavior unless the repository owner explicitly decides otherwise.
Rules:
- avoid silent breakage in public API or documented behavior
- prefer additive changes for minor releases
- reserve patch releases for fixes, small quality improvements, and non-breaking corrections
Record a breaking change only when user-facing behavior changes in a meaningful way, for example:
- public API signatures change
- documented behavior changes
- route syntax or matching behavior changes
- build output or runtime expectations change
- migration work is required in downstream projects
Do not record a change as breaking when:
- the implementation changes internally but the public contract stays the same
- code is refactored without changing observable behavior
- internal structure changes but generated/public behavior is preserved
Avoid low-signal or redundant implementation patterns.
Do not introduce pass-through wrappers or empty forwarding helpers unless they provide real value, such as:
- a stable public API boundary
- target-specific behavior
- meaningful normalization or validation
- future-proofing that is already justified by current architecture
Bad pattern:
- a function whose only purpose is to call another function with the same arguments and no additional contract
Do not duplicate logic across files or targets when the behavior is the same.
Prefer:
- extracting a shared helper
- moving shared logic downward into a common layer
- keeping target-specific layers thin when behavior is identical
Duplication is only acceptable when:
- target/runtime constraints genuinely differ
- keeping code separate materially improves clarity
- abstraction would be more confusing than the duplication
Unless the task is clearly docs-only or otherwise exempt, use this default execution flow for features, fixes, and implementation PRs:
- investigate the relevant code, tests, docs, and upstream references
- write a local temporary spec markdown file describing the intended change
- add or update the relevant tests first
- run the targeted tests and confirm they fail so the test is proven effective
- implement the feature or fix
- run the targeted tests again and confirm they pass
- run the full test suite to check for regressions
- run formatting
- run the analyzer and ensure there are no issues
- delete the local temporary spec markdown file before finishing
Default command sequence, adjusted as needed for the task:
dart test <targeted-tests>
dart test
dart format .
dart analyzeImplementation flow notes:
- the temporary spec file is a local working artifact, not a committed project document unless the task explicitly calls for that
- the red-to-green test step is important for behavior changes and bug fixes
- if a task is docs-only, release-only, or otherwise not test-driven by nature, choose a lighter process intentionally instead of forcing the default flow
- if formatting or analyzer scope can be narrowed safely, that is acceptable, but the final state should still be clean
Use Conventional Commits.
Preferred types:
featfixdocsrefactorperftestbuildcichore
Use a scope when it improves clarity.
Examples:
feat(routing): adopt roux 0.5 path syntaxfix(scanner): reject param-name drift in route shapesdocs(changelog): align v8 entry with release notes
Use BREAKING CHANGE: in the footer when a commit introduces a breaking API or behavior change.
PR titles should also use Conventional Commit style.
Examples:
feat(http): upgrade to ht 0.3 and osrv 0.6chore(release): prepare v8.0.0
Default PR body rule:
- leave the body empty unless the PR directly resolves a tracked issue
If the PR resolves an issue, use:
Resolves #<id>
CHANGELOG.md is the long-lived project record.
Each release entry should use this structure:
## vX.Y.Z
**Migration guide**: <link-if-needed>
### Highlights
Short release summary.
### Breaking Changes
- User-facing breaking changes only.
### What's New
#### <Area>
- User-facing additions and improvements.
### Migration note
- Concrete upgrade actions.
### Full Changelog
- Compare link.Changelog writing rules:
- optimize for historical accuracy over marketing copy
- group changes by user-facing area, not by internal commit order
- include author attribution and source references when useful
- prefer PR links when available
- use commit links only when work landed without a PR
- do not trust an existing unreleased section blindly when preparing a real release
- before publishing, rebuild the release entry from the actual diff between the previous released tag and the new release target
- verify the final release notes against real commits, merged PRs, and user-facing behavior changes
Attribution format in CHANGELOG.md:
by [@medz](https://github.com/medz) in [#157](https://github.com/medz/spry/pull/157)by [@medz](https://github.com/medz) in [13fed0d](https://github.com/medz/spry/commit/13fed0d99e266f138ac84d62d44a4014229070c1)
GitHub Releases use the same structure and ordering as CHANGELOG.md:
HighlightsBreaking ChangesWhat's NewMigration GuideFull Changelog
Release writing rules:
- optimize for the current release announcement, not for long-term archival wording
- keep the summary tighter than the changelog entry
- describe real user-facing changes only
- avoid raw commit lists as the main body
- do not assume a prewritten unreleased section is correct enough for publishing
- reconstruct the release summary from the actual range between the last released version and the new tag
Attribution format in GitHub Releases:
- prefer native GitHub mentions such as
@medz - prefer PR references such as
#157 - use commit hashes only when no PR exists
The changelog and release body should contain the same substantive change summary.
The main formatting difference is attribution style:
CHANGELOG.mdshould use explicit markdown links for people, PRs, and commits- GitHub Releases should use native GitHub references such as
@userand#123so GitHub links them automatically
Before publishing a new version:
- update
pubspec.yaml&lib/version.dart - finalize the release entry in
CHANGELOG.md - update migration docs when the release has breaking changes
- verify docs and release-facing branding if versioned wording exists
- run
dart test - run
dart pub publish --dry-run - build the docs site
- check CI on
main - smoke-test key examples when the release changes runtime/build behavior
- create and push the release tag
- publish to pub.dev
- create or update the GitHub Release body using the same structure