pub.dev revali is live on pub

Annotate your Dart.
Ship the whole API.

Revali reads the annotations on your classes and generates the server, a type-safe Dart client, an OpenAPI document and a production Dockerfile. You write business logic — revali dev writes the rest, and hot reloads it.

Pure Dart, no build_runner Hot reload MIT licensed
routes/users_controller.dart
yours
@Controller('users')
class UsersController {
  const UsersController(this.users);

  final UserService users;

  @Get()
  Future<List<User>> all() => users.all();

  @Get(':id')
  Future<User> byId(@Param() String id) {
    return users.find(id);
  }

  @Post()
  @StatusCode(201)
  Future<User> create(@Body() NewUser body) {
    return users.create(body);
  }
}
Server
.revali/server
Dart client
.revali/revali_client
OpenAPI 3.0.3
swagger.yaml
Dockerfile
.revali/build
One source of truth

Write it once. Generate the rest.

Your controller is the only place an endpoint is described. Everything downstream is derived from it, so nothing can drift out of sync — not the client, not the docs, not the deploy.

dart run revali routes
generated
prefix: /api  (4 routes)

GET      /users        →  UsersController.all
GET      /users/:id    →  UsersController.byId
POST     /users        →  UsersController.create
Request lifecycle

The return type is the hook.

No registration table, no ordering config, no base class per concern. Write a method on a LifecycleComponent, and what it returns decides where in the request it runs.

RequestA request arrives at your server
1 WrapperWrap the whole request — timing, tracing, transactions WrapperResult
2 MiddlewareRead it, add to it, short-circuit it MiddlewareResult
3 GuardLet it through, or stop it here GuardResult
4 InterceptorLast look before the handler runs InterceptorPreResult
5 EndpointYour method. Arguments already parsed and typed
6 InterceptorShape the response on the way out InterceptorPostResult
ResponseSerialized and sent
lib/components/session.dart
yours
class Session implements LifecycleComponent {
  const Session(this.tokens);

  final TokenService tokens;

  // A Guard: it can stop the request.
  Future<GuardResult> authenticated(
    @Header('authorization') String? auth,
  ) async {
    if (await tokens.valid(auth)) {
      return const GuardResult.pass();
    }

    return const GuardResult.block(statusCode: 401);
  }

  // An Interceptor: it runs after the handler.
  InterceptorPostResult timing(Context context) {
    context.response.headers.add('x-served-by', 'revali');
    return const InterceptorPostResult.next();
  }
}
Batteries included

Everything a real API needs, already wired up.

Binding, validation, dependency injection, realtime, CORS, exception handling. Not a routing library you assemble a framework around.

Typed request binding

Query, path, header, cookie and body params are parsed and typed before your method runs. A bad request never reaches it — it is a 400 with a real message.

@Get('search')
Future<Page<User>> search(
  @Query() String q,
  @Query('page') int page,
  @Header('accept-language') String? locale,
  @Cookie('session') String? session,
) async {
  // Parsed, typed, and validated before you
  // are called. A bad request never gets here
  // — it is a 400 with a real message.
  return users.search(q, page: page);
}

Dependency injection built in

Register singletons, lazy singletons and factories on your app config. Constructors are injected — including your lifecycle components.

@App()
final class MainApp extends AppConfig {
  const MainApp() : super(port: 8080);

  @override
  Future<void> configureDependencies(DI di) async {
    di.registerSingleton(await Database.connect());
    di.registerLazySingleton<UserService>(UserServiceImpl.new);
    di.registerFactory<Mailer>(SmtpMailer.new);
  }
}

WebSockets

Annotate a method. Two-way, receive-only or send-only.

@Controller('chat')
class ChatController {
  const ChatController();

  @WebSocket('messages')
  String handle(@Body() String message) {
    return 'Echo: $message';
  }
}

Server-sent events

Return a Stream and Revali handles the rest, cleanup included.

@Controller('events')
class EventController {
  const EventController();

  @SSE('live')
  Stream<String> live(CleanUp cleanUp) async* {
    cleanUp.add(feed.dispose);

    yield* feed.stream;
  }
}

Write your own construct

Everything above is a construct. Yours gets the same input.

class GraphQlConstruct implements BuildConstruct {
  @override
  Future<void> build(
    RevaliContext context,
    List<MetaRoute> routes,
  ) async {
    // Every route, every annotation, every type
    // — the same input the built-in server uses.
  }
}

Access control

CORS origins, required and forbidden headers, and pre-flight handling, declared with @AllowOrigins and friends.

Pipes & transformation

Convert a raw String id into a real User before your handler sees it, in one annotation.

Scaffolding

revali create generates controllers, apps, pipes, observers and lifecycle components.

The dev loop

One command. Then just write code.

No build_runner in a second terminal, no codegen step to remember, no restart to sit through. Save the file; the routes are regenerated and the server hot reloads.

zsh — revali dev
$dart run revali dev
12:34:56 PM [READY]
Serving at http://localhost:8080/api
Press: r reload, c clear, q quit
/users
GET -> /users/
12:35:02 PM [RELOAD]
routes/users_controller.dart changed
Serving at http://localhost:8080/api
$

Hot reload

Changes in routes/ and lib/components/ reload the running server in place.

Real debugging

A Dart VM service is running, so attach your IDE and set breakpoints in generated code too.

See your routes

revali routes prints the route table straight from the generated manifest.

Diagnose it

revali doctor checks the kernel, constructs and generated output.

Quickstart

Running in under a minute.

1 Add the packages
dart pub add revali --dev
dart pub add revali_router
2 Write a controller
@Controller('hello')
class HelloController {
  const HelloController();

  @Get()
  String world() => 'Hello, world!';
}
3 Run it
dart run revali dev

Stop writing the parts a compiler could write.

Revali is open source and MIT licensed. Your annotations are the spec — everything else is generated.

$ dart pub add revali --dev