Annotate your Dart.
Generate the server.

Revali is a Dart backend framework. It reads the annotations on your classes and generates the server. Add a construct for a type-safe Dart client, an OpenAPI document or a production Dockerfile.

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_client_gen
OpenAPI 3.0.3
+ revali_swagger
Dockerfile
+ revali_docker
One source of truth

Write it once. Generate the rest.

Your controller is the only place an endpoint is described. The server and any construct you add read it from there, so the client, the docs and the deploy stay in step.

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

GET     /api/users      →  UsersController.all
GET     /api/users/:id  →  UsersController.byId
POST    /api/users      →  UsersController.create
lib/main.dart (your Flutter app)
generated
// .revali/revali_client — generated, then added
// to your Flutter app as a path dependency.
import 'package:client/client.dart';
import 'package:models/models.dart';

final server = Server();

// The same names. The same types.
// No HTTP written by hand, on either side.
final users = await server.users.all();

final ada = await server.users.create(
  body: NewUser(
    name: 'Ada',
    email: 'ada@revali.dev',
  ),
);
.revali/revali_swagger/swagger.yaml
generated
openapi: 3.0.3
info:
  title: API
  version: 1.0.0
paths:
  /users:
    get:
      operationId: users_all
      tags:
        - users
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
    post:
      operationId: users_create
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewUser'
.revali/build/Dockerfile
generated
# Stage 1: Build environment
FROM dart:stable AS build

WORKDIR /app
COPY . .
RUN dart pub get

# Build the server with Revali
RUN dart run revali build --release

# Compile to native executable
RUN dart compile exe .revali/server/server.dart \
  -o /app/server

# Stage 2: Runtime environment
FROM alpine:latest
RUN apk add --no-cache libc6-compat ca-certificates
COPY --from=build /app/server /app/bin/server

CMD ["/app/bin/server"]
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['x-served-by'] = 'revali';
  }
}
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 {
  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.

final class GraphQlConstruct extends BuildConstruct {
  const GraphQlConstruct();

  @override
  BuildDirectory generate(
    RevaliBuildContext context,
    MetaServer server,
  ) {
    // Every route, every annotation, every type
    // — the same input the built-in server uses.
    return BuildDirectory(files: const []);
  }
}

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 three steps.

You need Dart 3.8 or later.

1 Create a project
dart create -t console my_api
cd my_api
dart pub add revali_router
dart pub add --dev revali
2 Write a controller
import 'package:revali_router/revali_router.dart';

@Controller('hello')
class HelloController {
  const HelloController();

  @Get()
  String world() => 'Hello, world!';
}

Save it as routes/controllers/hello_controller.dart

3 Run it
dart run revali dev

Then open http://localhost:8080/api/hello

Stop writing the parts a compiler could write.

Revali is open source and MIT licensed. Your annotations are the spec, and the server is generated from them.

$ dart pub add revali_router dev:revali