Coouter is a high-level abstraction layer for Shelf designed for developers who demand type safety, declarative architecture, and automatic documentation.
Eliminate JSON validation boilerplate, manual error handling, and Swagger synchronization. With Coouter, your code is your documentation.
- 🛡️ Total Type Safety: Native integration with
fpdartusing theEither<Failure, Success>pattern. - 🧱 Declarative Architecture: Organize your logic into reusable Controllers, Handlers, and Routers.
- 📝 Swagger/OpenAPI 3.0: Automatic specification generation and included Swagger UI server.
- 🔄 Smart Mapping: Automatic conversion of Body and Query Params to Dart models with integrated validation.
- 🧩 Middleware Composition: Apply cross-cutting logic at the router, controller, or individual route level.
- 🚀 Shelf-Ready: Fully compatible with the entire Shelf middleware ecosystem.
Add coouter to your pubspec.yaml:
dependencies:
coouter: ^1.0.0Or run it in your terminal:
dart pub add coouterCoouter is built on three fundamental structures to organize your API:
Ideal for rapid prototyping or simple endpoints.
final healthCheck = ApiControllerHandler<MyError, JsonResponse>(
verb: HttpMethod.GET,
path: '/health',
handler: (request) async => Right(JsonResponse({'status': 'alive'})),
);Perfect for complex logic and reusability. Allows for dependency injection and state management.
class GetUserController extends ApiConverterController<UserParams, AppError, JsonResponse> {
GetUserController() : super(verb: HttpMethod.GET, path: '/users/:id');
@override
UserParams paramsMapper(Map<String, dynamic> map) => UserParams.fromMap(map);
@override
get processConvertedRequest => (ctx, request) async {
final user = await repository.findById(ctx.params.id);
return user != null
? Right(JsonResponse(user.toMap()))
: Left(AppError.notFound('User not found'));
};
}Group controllers under common prefixes and middlewares.
class ApiV1 extends ApiRouter {
@override
List<BaseMiddleware> get middlewares => [AuthMiddleware()];
@override
Map<String, ApiMountable> get controllers => {
'/users': ApiMountable.multiple([GetUserController(), CreateUserController()]),
'/status': ApiMountable.single(healthCheck),
};
}Forget about infinite try-catch blocks. Coouter uses Either to force you to handle errors explicitly. Define your failures by extending ResponseFailure:
class AppError extends ResponseFailure {
const AppError(this.message, {super.statusCode = 500});
@override
final String? message;
factory AppError.notFound(String msg) => AppError(msg, statusCode: 404);
}Add the SwaggerInfo mixin to your controllers to automatically generate the OpenAPI specification:
class CreateUserController extends ApiConverterWithBodyController<UserBody, NoParams, AppError, JsonResponse>
with SwaggerInfo {
@override
String get description => 'Creates a new user in the system';
@override
Map<int, ResponseDoc> get responses => {
201: ResponseDoc('User created successfully', schema: SchemaDoc.name('User')),
400: ResponseDoc('Invalid data'),
};
}To serve the documentation, simply add the SwaggerController:
final router = Router()
..mount('/api/v1', apiV1.handler)
..mount('/docs', SwaggerController(apiRouter: apiV1).handler);Access http://localhost:8080/docs/ and you'll see your Swagger UI ready to use.
import 'package:coouter/coouter.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
void main() async {
final api = ApiV1(); // Your route group
final handler = const Pipeline()
.addMiddleware(logRequests())
.addHandler(api.handler);
await shelf_io.serve(handler, '0.0.0.0', 8080);
print('🚀 Server flying at http://localhost:8080');
}Contributions are welcome! If you have an idea for a new feature or have found a bug, please open an Issue or a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.