Typesafe APIs for Dart, inspired by tRPC and powered by Schemantic.
- End-to-end typesafety: Share your schema and procedure definitions between client and server.
- Request Batching: Automatic batching of queries and mutations into a single HTTP request.
- Streaming: Built-in support for streaming queries and mutations with valid NDJSON responses.
- Server Agnostic: Run on top of Shelf, bare
dart:io, or any other compatible HTTP framework. - Runtime validation: Inputs and outputs are validated automatically at runtime.
- Autocompletion: IDE support for your API routes.
Create a shared library containing your data schemas and procedure definitions.
// shared.dart
import 'package:zrpc/zrpc.dart';
part 'shared.g.dart';
@Schematic()
abstract class UserSchema {
String get id;
String get name;
bool get isAdmin;
}
// Define procedures with their input/output types
final greeting = ZRpc.query(
'greeting',
input: stringType(),
output: stringType(),
);
final updateUser = ZRpc.mutation(
'updateUser',
input: UserType,
output: UserType,
);
final streamTicker = ZRpc.streamQuery(
'ticker',
input: intType(), // tick count
output: intType(), // current tick
);Implement your API procedures.
// server.dart
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:zrpc/zrpc.dart';
import 'package:zrpc/adapters/shelf.dart';
import 'shared.dart';
class Context {
final Request request;
Context({required this.request});
}
final appRouter = router<Context>()
.query(greeting, (ctx, name) async {
return 'Hello, $name!';
})
.mutation(updateUser, (ctx, user) async {
// Perform update...
return user;
})
.streamQuery(streamTicker, (ctx, count) async* {
for (int i = 0; i < count; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
});
void main() async {
final handler = zrpcShelfHandler(
router: appRouter,
createContext: (request) => Context(request: request),
);
await shelf_io.serve(handler, 'localhost', 8080);
print('Server running on localhost:8080');
}Use the shared definitions to call the API in a typesafe way. Queries executed in the same microtask are automatically batched!
// client.dart
import 'package:zrpc/client.dart';
import 'shared.dart';
void main() async {
final client = ZRpcClient('http://localhost:8080');
// Fully typed based on the definition!
final result = await client.query(greeting, 'World');
print(result); // "Hello, World!"
// Complex types work automatically
final user = await client.mutation(
updateUser,
User.from(id: '1', name: 'New Name', isAdmin: false),
);
print(user.name);
// Streaming works too!
await for (final tick in client.streamQuery(streamTicker, 5)) {
print('Tick: $tick');
}
}ZRpc automatically groups queries and mutations made within the same microtask execution into a single HTTP request (POST /batch).
- Reduced Overhead: Fewer HTTP connections.
- Concurrent Execution: The server processes batched requests concurrently.
- Smart Streaming: Responses are streamed back via NDJSON as they complete, preventing head-of-line blocking.
// These two requests will be sent in a ONE HTTP call
Future.wait([
client.query(greeting, 'One'),
client.query(greeting, 'Two'),
]);dart pub add zrpc