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.
Prerequisites
Section titled “Prerequisites”- Dart SDK 3.3 or later.
- Basic familiarity with gisila services and forms; see the framework section.
1. Add dependencies
Section titled “1. Add dependencies”name: users_apienvironment: 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.0dart pub get2. Write a model
Section titled “2. Write a model”gisila_doc derives the schema from the constructor; jsonResponse calls toJson() when it
encodes the result.
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, };}3. Write a form and a service
Section titled “3. Write a form and a service”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];}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'); }}4. Write the controller
Section titled “4. Write the controller”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:
idmatches{id}, so it is a required path parameter.limitandoffsetare nullable primitives, so they are optional query parameters.UserServiceextendsService, so it is injected and not documented.CreateUserFormextendsForm, so it is bound withbindFormand documented as the request body (JSON or form-encoded).Future<User>gives aUserresponse schema;Future<void>on@Deletegives an empty204.- Every route that binds input also documents a
400with gisila’s error envelope.
5. Generate code
Section titled “5. Generate code”dart run build_runner buildThis writes lib/controllers/users_controller.g.dart with a UsersControllerGisilaDoc
extension that adds attachToApp(app, router, spec, {prefix}).
6. Wire up the server
Section titled “6. Wire up the server”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');}Verify it works
Section titled “Verify it works”dart run bin/server.dartcurl -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 -20Open 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.
Next steps
Section titled “Next steps”- Describe request and response models in more detail.
- Document authentication for guarded routes.
- Generate API clients from the spec.