Spry

Test Pub Version GitHub license X (twitter) Documentation Netlify Status

File-routing Dart server framework for teams that want one codebase across Dart VM, Node.js, Bun, Deno, Cloudflare Workers, Vercel, and Netlify.

Spry is built for a specific job:

  • write server routes as files, not imperative registration code
  • keep generated runtime output explicit and inspectable
  • build the same project for multiple runtime targets
  • generate OpenAPI documents and typed clients from the same source tree

If you want a Dart server framework that stays close to the filesystem, keeps deployment flexible, and does not hide the runtime behind a giant DSL, Spry is the fit.

Why Spry

  • File routing first: routes/, middleware/, _middleware.dart, and _error.dart define the server shape directly from the project tree.
  • Cross-runtime by design: target Dart VM, native snapshots, Node.js, Bun, Deno, Cloudflare Workers, Vercel, and Netlify without rewriting route code.
  • Inspectable generated output: Spry emits concrete runtime files instead of burying behavior inside a black box.
  • OpenAPI and client generation: keep API contracts, docs, and first-party typed clients aligned with the same route tree.

Start Here

  • Quick start: install Spry, add routes/, add spry.config.dart, run dart run spry serve
  • Routing guide: learn params, wildcards, scoped middleware, and error boundaries
  • Deploy guide: see how the same project targets Dart, Node, Bun, Deno, Cloudflare, Vercel, and Netlify
  • Client and OpenAPI: generate API docs and typed clients from the same app model

Quick Start

Install the package:

dart pub add spry

Create a minimal project structure:

.
├─ routes/
│  └─ index.dart
└─ spry.config.dart

spry.config.dart

import 'package:spry/config.dart';

void main() {
  defineSpryConfig(
    host: '127.0.0.1',
    port: 4000,
    target: BuildTarget.vm,
  );
}

routes/index.dart

import 'package:spry/spry.dart';

Response handler(Event event) {
  return Response.json({
    'message': 'hello from spry',
    'runtime': event.context.runtime.name,
    'path': event.url.path,
  });
}

Start the dev server:

dart run spry serve

AI Inspection

Spry 8.6.0 includes local MCP inspection over stdio and opt-in loopback HTTP.

Run the MCP server from your project directory over stdio:

dart run spry mcp --root .

The inspection tools report effective config, scanned routes, filesystem middleware and error handlers, and configured OpenAPI and client output locations. Standalone inspections reload project config and rescan on each tool request; use trusted projects.

To run the local endpoint alongside the dev server, add mcp to defineSpryConfig:

mcp: McpConfig(enable: true, port: 4001),

Connect a native MCP client using Streamable HTTP to the loopback URL printed by spry serve. The default MCP port is the app port plus one, independently of the app's bind host. The endpoint supports MCP 2025-06-18 with JSON POST responses and empty 202 responses to notifications. GET returns 405 because it provides no SSE stream or server-initiated messages; it does not implement the older HTTP+SSE transport. Clients send the negotiated MCP-Protocol-Version header on subsequent requests. Browser origins are limited to loopback at the MCP port; requests without an Origin header are accepted for native clients.

The HTTP endpoint is intended for opt-in local development. It has no authentication, so other local processes can connect. Serve sessions refresh scanned state and effective config on successful rebuilds and close the endpoint when the app runner exits. OpenAPI and client status tools describe configured output locations; inspect generated files separately. See the official transport specification and the bundled Spry guidance.

Core Ideas

  • routes/ defines request handlers with file routing
  • middleware/ and _middleware.dart shape cross-cutting request behavior
  • _error.dart provides scoped error handling
  • defineHandler(...) adds handler-local middleware and error handling
  • public/ serves static assets directly
  • spry.config.dart selects the runtime target and build behavior

Local static-file serving validates request paths lexically and follows filesystem symlinks. Keep the configured publicDir tree, including symlink targets, under your control.

What You Ship

With Spry, the authoring model stays small:

  • handlers return Response values directly
  • route structure comes from folders and filenames
  • scoped middleware and errors stay near the routes they affect
  • config decides the runtime target instead of per-route branching

What Spry generates:

  • a concrete app definition you can inspect
  • runtime entry files for the selected target
  • target-specific wrappers for platforms like Cloudflare Workers or Vercel

OpenAPI

Spry can generate an openapi.json document as part of the normal build pipeline.

Use package:spry/config.dart for the build-side config and package:spry/openapi.dart for the document objects:

import 'package:spry/config.dart';
import 'package:spry/openapi.dart';

void main() {
  defineSpryConfig(
    openapi: OpenAPIConfig(
      document: OpenAPIDocumentConfig(
        info: OpenAPIInfo(title: 'Spry API', version: '1.0.0'),
      ),
      output: OpenAPIOutput.route('openapi.json'),
    ),
  );
}

Route files can expose top-level openapi metadata:

import 'package:spry/openapi.dart';

final openapi = OpenAPI(
  summary: 'List users',
  tags: ['users'],
);

Key rules:

  • OpenAPIConfig.document.components defines document-level components.
  • Route-level OpenAPI(..., globalComponents: ...) is lifted into document components during generation.
  • A route without a method suffix expands to GET, POST, PUT, PATCH, DELETE, and OPTIONS in OpenAPI.
  • HEAD is only emitted when a route explicitly defines .head.dart.
  • OpenAPIOutput.route('openapi.json') writes the file into public/, so it is served like any other static asset.

Runtime Targets

Spry can emit output for:

Target Runtime Deploy Docs
vm Dart VM Dart VM
exe Native executable Native executable
aot AOT snapshot AOT snapshot
jit JIT snapshot JIT snapshot
kernel Kernel snapshot Kernel snapshot
node Node.js Node.js
bun Bun Bun
deno Deno Deno
cloudflare Cloudflare Workers Cloudflare Workers
vercel Vercel Vercel
netlify Netlify Functions Netlify Functions

WebSockets

Spry exposes websocket upgrades from the request event without introducing a second routing system.

import 'package:spry/spry.dart';
import 'package:spry/websocket.dart';

Response handler(Event event) {
  if (!event.ws.isSupported || !event.ws.isUpgradeRequest) {
    return Response('plain http fallback');
  }

  return event.ws.upgrade((ws) async {
    ws.sendText('connected');

    await for (final message in ws.events) {
      switch (message) {
        case TextDataReceived(text: final text):
          ws.sendText('echo:$text');
        case BinaryDataReceived():
        case CloseReceived():
          break;
      }
    }
  }, protocol: 'chat');
}

Current websocket support follows the underlying osrv runtime surface:

  • supported: Dart VM, Node.js, Bun, Deno, Cloudflare Workers
  • unsupported: Vercel, current Netlify Functions runtime

Documentation

Ask DeepWiki

Read the documentation at spry.medz.dev.

Start here:

License

MIT

Sponsors

Spry framework is an MIT licensed open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider sponsoring Seven(@medz) development.

sponsors

Contributing

Thank you to all the people who already contributed to Spry!

Contributors