Skip to content

Document an API

You will build a users API with list, get, create and delete routes, generate the routing and OpenAPI code with build_runner, and open the result in Swagger UI at /docs.

  • Dart SDK 3.3 or later.
  • Basic familiarity with gisila services and forms; see the framework section.
pubspec.yaml
name: users_api
environment:
sdk: ^3.3.0
dependencies:
gisila_doc: ^0.3.1
shelf: ^1.4.1
shelf_router: ^1.1.4
dev_dependencies:
build_runner: ^2.5.0
Terminal window
dart pub get

gisila_doc derives the schema from the constructor; jsonResponse calls toJson() when it encodes the result.

lib/models/user.dart
class User {
User({required this.id, required this.name, required this.email, this.age});
final String id;
final String name;
final String email;
final int? age;
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
if (age != null) 'age': age,
};
}
lib/forms/user_forms.dart
import 'package:gisila_doc/gisila_doc.dart';
class CreateUserForm extends Form {
final name = StringField(name: 'name', required: true, maxLength: 100);
final email = EmailField(name: 'email', required: true);
final age = IntField(name: 'age', min: 0, max: 150);
@override
List<FormField<Object?>> collectFields() => [name, email, age];
}
lib/services/user_service.dart
import 'package:gisila_doc/gisila_doc.dart';
import '../models/user.dart';
class UserService extends Service {
static final _store = <String, User>{};
Future<List<User>> list({int? limit, int? offset}) async {
final all = _store.values.toList();
final start = (offset ?? 0).clamp(0, all.length);
final end = limit == null ? all.length : (start + limit).clamp(0, all.length);
return all.sublist(start, end);
}
Future<User> getById(String id) async =>
_store[id] ?? (throw NotFound('User "$id" not found'));
Future<User> create({required String name, required String email, int? age}) async {
final user = User(id: '${_store.length + 1}', name: name, email: email, age: age);
return _store[user.id] = user;
}
Future<void> delete(String id) async {
if (_store.remove(id) == null) throw NotFound('User "$id" not found');
}
}
lib/controllers/users_controller.dart
import 'package:gisila_doc/gisila_doc.dart';
import '../forms/user_forms.dart';
import '../models/user.dart';
import '../services/user_service.dart';
part 'users_controller.g.dart';
@Controller('/users', ['Users'])
class UsersController {
@Get('/', summary: 'List users', description: 'Returns a page of users.')
Future<List<User>> listUsers(UserService users, {int? limit, int? offset}) =>
users.list(limit: limit, offset: offset);
@Get('/{id}', summary: 'Fetch a user', operationId: 'getUserById')
@ApiResponse(404, description: 'User not found')
Future<User> getUser(String id, UserService users) => users.getById(id);
@Post('/', summary: 'Create a user', operationId: 'createUser')
Future<User> createUser(CreateUserForm form, UserService users) => users.create(
name: form.name.value!,
email: form.email.value!,
age: form.age.value,
);
@Delete('/{id}', summary: 'Delete a user', operationId: 'deleteUser')
@ApiResponse(404, description: 'User not found')
Future<void> deleteUser(String id, UserService users) => users.delete(id);
}

What the generator infers here:

  • id matches {id}, so it is a required path parameter.
  • limit and offset are nullable primitives, so they are optional query parameters.
  • UserService extends Service, so it is injected and not documented.
  • CreateUserForm extends Form, so it is bound with bindForm and documented as the request body (JSON or form-encoded).
  • Future<User> gives a User response schema; Future<void> on @Delete gives an empty 204.
  • Every route that binds input also documents a 400 with gisila’s error envelope.
Terminal window
dart run build_runner build

This writes lib/controllers/users_controller.g.dart with a UsersControllerGisilaDoc extension that adds attachToApp(app, router, spec, {prefix}).

bin/server.dart
import 'package:gisila_doc/gisila_doc.dart';
import 'package:users_api/controllers/users_controller.dart';
import 'package:users_api/services/user_service.dart';
Future<void> main() async {
final spec = OpenApiSpec(
info: const ApiInfo(
title: 'Users API',
version: '1.0.0',
description: 'A tiny API documented by gisila_doc.',
),
servers: const [ApiServer(url: 'http://localhost:8080')],
);
final app = GisilaApp(config: AppConfig());
app.registerService<UserService>(UserService.new);
app.registerController(attacher: (app, router, {String prefix = ''}) {
UsersController().attachToApp(app, router, spec, prefix: prefix);
// Mounted after the controllers, so their routes match first.
router.mount('/', docsHandler(spec, title: 'Users API'));
});
await app.start(port: 8080);
print('Docs at http://localhost:8080/docs');
}
Terminal window
dart run bin/server.dart
Terminal window
curl -s -X POST localhost:8080/users \
-H 'content-type: application/json' \
-d '{"name":"Ada","email":"ada@example.com"}'
# {"id":"1","name":"Ada","email":"ada@example.com"} (status 201)
curl -s localhost:8080/openapi.json | head -20

Open http://localhost:8080/docs: the Users tag lists four operations, each with its parameters, request body and responses, and “Try it out” calls the running server.