Skip to main content

winter 1.0.0-rc.2 copy "winter: ^1.0.0-rc.2" to clipboard
winter: ^1.0.0-rc.2 copied to clipboard

retracted

A Spring-inspired backend framework for Dart on dart:io - routing, filters, DI, JSON mapping, validation, security and consistent errors.

Winter ❄️ #

A backend framework for the Dart enthusiasts #

Winter is a backend framework for Dart, inspired by Spring, ASP.NET Core and NestJS. It gives you routing, filters, dependency injection, JSON mapping, validation, security and consistent errors, on top of dart:io and without code generation.

⚠️ Winter is an experimental project, built in our spare time. 1.0.0-rc.2 is a release candidate: the API is the one we intend to keep in 1.0, but it may still change before it, and the framework has been used by one real application so far (feather-io). Try it, build with it, and tell us what breaks; use it in production at your own discretion.

What Winter is:

  • A framework for REST APIs: routes, filters, JSON in and out, validation, errors, security, configuration, logs, scheduled tasks, OpenAPI, WebSockets and Server-Sent Events.
  • Small and explicit: no reflection, no annotations, no code generation; what you register is what runs.
  • Tested and documented: about 1200 tests (99.7 % of the lines), a guide per module and an example for every feature.

What Winter is not (yet):

  • Proven in production: no app runs on it at scale yet, so expect rough edges.
  • A full-stack framework: no database access (bring your own driver), templates, sessions, CSRF protection nor OAuth flows.
  • A distributed system: the rate limiter and the scheduled tasks live in one process; several instances need a shared store of their own.

Now that it's clear what Winter is, let's set it up.

Getting started #

Install it #

You need the Dart SDK 3.13 or newer (get Dart) and a Dart project (dart create my_api). Then add Winter:

dart pub add winter:^1.0.0-rc.2

The version is needed while 1.0.0 is a release candidate: dart pub add winter alone only picks stable versions. That's all the setup there is: no generator to run, no configuration file.

Your first server #

With Winter installed, the smallest server is one route:

import 'package:winter/winter.dart';

void main() async {
  await Winter.start(
    router: WinterRouter(
      routes: [
        Route.get(path: '/hello', handler: (request) => ResponseEntity.ok(body: 'Hello Winter!')),
      ],
    ),
  );
}

Run it with dart run, and the server answers on port 8080 (the default):

curl localhost:8080/hello    # Hello Winter!

That's as simple as it gets, but Winter does a lot more.

Features #

Feature What you get Guide
Routing Nested routes, typed path and query params, regex, 404/405, HEAD and OPTIONS, static files, health checks routing
Filters Global and route filters, ordered, that see every error filters
Requests and responses Headers, cookies, every kind of body (JSON, forms, uploads, binary), streams, conditional requests (304/412/428), Prefer, Link pagination, idempotency keys, deprecation headers requests and responses
JSON toJson() without interfaces, your own serializers, generics, snake_case, clear errors object mapper
Validation Typed validators, nested objects, async rules, a 422 with a code per field validation
Errors Every error is a Problem Details (RFC 9457); a 500 never leaks details error handling
Security Authentication filters, roles and permissions, CORS, security headers, a rate limiter security
Dependency injection Singletons, lazy, factories, one instance per request, async startup, disposal on shutdown dependency injection
Configuration Typed environment variables, .env files and profiles configuration
Logging Console and JSON loggers, a request id and the W3C trace in every log logging
i18n Messages in the language of the request i18n
Real time WebSockets that go through the filters first, and Server-Sent Events routing, SSE
OpenAPI The document of the routes and Swagger UI, schemas from an example and its validate() openapi
Scheduled tasks Intervals and cron expressions, started and stopped with the server scheduling
Testing The whole pipeline in memory, without ports testing
Deployment Native executables, Docker, graceful shutdown, health checks deployment

How it fits together: architecture. Every guide: doc/.

A real API #

Now something closer to what an API does every day: a JSON body turned into an object and validated, a service from dependency injection, a filter that every request goes through, and errors that the client can read.

import 'package:winter/winter.dart';

/// The body of `POST /tasks`: read from JSON, then validated
class NewTask implements Validatable {
  final String title;
  final int priority;

  NewTask(this.title, this.priority);

  factory NewTask.fromJson(Map<String, dynamic> json) =>
      NewTask(json.field<String>('title'), json.field<int?>('priority') ?? 3);

  @override
  ConstraintValidatorContext validate() => ConstraintValidatorContext()
    ..field('title', title).notBlank().size(max: 100)
    ..field('priority', priority).min(1).max(5);
}

/// What the API answers: written as JSON with its toJson()
class Task {
  final int id;
  final String title;
  final int priority;

  Task(this.id, this.title, this.priority);

  Map<String, Object> toJson() => {'id': id, 'title': title, 'priority': priority};
}

class TaskService {
  final Map<int, Task> _tasks = {};

  List<Task> all() => _tasks.values.toList();

  Task find(int id) => _tasks[id] ?? (throw NotFoundException(detail: 'Task $id not found'));

  Task create(NewTask task) {
    final int id = _tasks.length + 1;
    return _tasks[id] = Task(id, task.title, task.priority);
  }
}

/// A filter: every request must say which app sends it
class ClientFilter extends Filter {
  @override
  Future<ResponseEntity> doFilter(RequestEntity request, FilterChain chain) async {
    if (request.headers['x-client'] == null) {
      throw const BadRequestException(detail: 'Send the X-Client header');
    }
    return chain.doFilter(request);
  }
}

WinterRouter router() => WinterRouter(
  basePath: '/tasks',
  routes: [
    Route.get(
      path: '/',
      handler: (request) => ResponseEntity.ok(body: di.find<TaskService>().all()),
    ),
    Route.get(
      path: '/{id|[0-9]+}',
      handler: (request) => ResponseEntity.ok(
        body: di.find<TaskService>().find(request.pathParam<int>('id')),
      ),
    ),
    Route.post(
      path: '/',
      handler: (request) async {
        final NewTask newTask = await request.body<NewTask>(); // 400 or 422 if it's wrong
        final Task task = di.find<TaskService>().create(newTask);
        return ResponseEntity.created(location: '/tasks/${task.id}', body: task);
      },
    ),
  ],
);

void main() async {
  di.put(TaskService());
  Winter.context.setUp(
    objectMapper: ObjectMapper(deserializers: [Deserializer<NewTask>.json(NewTask.fromJson)]),
  );
  await Winter.start(
    globalFilterConfig: FilterConfig([LoggingFilter(), ClientFilter()]),
    router: router(),
  );
}

What a client gets (every error is a Problem Details, application/problem+json):

Request (with X-Client: web) Response
POST /tasks {"title": "Write the docs"} 201, Location: /tasks/1, {"id": 1, "title": "Write the docs", "priority": 3}
POST /tasks {"title": " ", "priority": 9} 422 with a violation per field: title (notBlank) and priority (max.inclusive)
POST /tasks {"priority": 2} 400, "detail": "$.title: missing"
GET /tasks/7 404, "detail": "Task 7 not found"
GET /tasks without X-Client 400, "detail": "Send the X-Client header"

And it's tested in memory, with the whole pipeline and without opening a port:

import 'package:test/test.dart';
import 'package:winter/winter.dart';

void main() {
  final client = WinterTestClient.build(
    globalFilterConfig: FilterConfig([ClientFilter()]),
    router: router(),
  );

  test('an invalid task is a 422', () async {
    final response = await client.post(
      '/tasks',
      headers: {'x-client': 'test'},
      body: {'title': ' '},
    );

    expect(response.statusCode, 422);
  });
}

From zero to this, step by step: getting started.

Examples #

If you liked it, example/ has much more: many small examples by topic, one file per case with its test, and complete apps.

Topic Cases
routing, bodies Params, nested routes, a CRUD, static files, health checks; every kind of request body
object_mapper, validation Field naming, adapters, PATCH, sealed classes; nested objects, custom and async rules
errors, filters Exceptions of the domain, a handler of your own; tenants, maintenance mode, a response cache
security, di API keys, Basic auth, webhook signatures, login throttling; scoped services, async startup
api_patterns Pagination, ETag and If-Match, content negotiation, downloads, 202 jobs, idempotency keys, Prefer, versioning
openapi, realtime, scheduling, i18n Swagger UI, Server-Sent Events, a WebSocket chat, cron tasks, translated messages
apps Authentication with JWT, an orders API, a photo gallery with uploads and a chat, a production setup with Docker

Powered by Winter #

The projects that run on Winter:

feather-io 🌰 ➕
feather-io Almond: coming soon 🤭 Your project?

More #

  • DECISIONS.md: why Winter works the way it does.
  • Winter and other frameworks: where its ideas come from (Spring, ASP.NET Core, Ktor, NestJS...) and how it compares with the Dart frameworks.
  • CHANGELOG.md and ROADMAP.md.
  • Benchmarks: Winter serves ~85 % of the requests per second of raw dart:io, and the object mapper costs about a tenth more than JSON written by hand.

Contributing #

Issues and pull requests are welcome. CONTRIBUTING.md explains how to set up the project (FVM), run the checks and the tests, add a language, and write the commits and the changelog.

License #

Apache 2.0.

2
likes
0
points
239
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

A Spring-inspired backend framework for Dart on dart:io - routing, filters, DI, JSON mapping, validation, security and consistent errors.

Repository (GitHub)
View/report issues

Topics

#server #http #backend #rest #api

License

unknown (license)

Dependencies

collection, http_parser, intl, slang

More

Packages that depend on winter