Create a project
You’ll create a small todo_api server with one controller, generate its routing code and call it with curl. At the end you have the standard gisila project layout to build on.
Prerequisites
Section titled “Prerequisites”- Dart SDK 3.3 or newer (
dart --version). - curl, or any HTTP client.
1. Create the package
Section titled “1. Create the package”dart create -t console todo_apicd todo_api2. Add dependencies
Section titled “2. Add dependencies”Edit pubspec.yaml:
name: todo_apipublish_to: none
environment: sdk: ^3.3.0
dependencies: gisila: ^0.4.1
dev_dependencies: build_runner: ">=2.5.0 <3.0.0" lints: ">=3.0.0 <7.0.0" test: ^1.24.0If you work from a local checkout of the gisila repository, use gisila: {path: ../gisila} instead. Then:
dart pub getpackage:gisila/gisila.dart re-exports what you need from shelf and shelf_router, so you don’t add them yourself.
3. Write a controller
Section titled “3. Write a controller”Create lib/controllers/todos_controller.dart:
import 'package:gisila/gisila.dart';
part 'todos_controller.g.dart';
@Controller('/todos')class TodosController { final Map<String, String> _todos = {};
@Get('/') Future<List<Map<String, Object?>>> list() async => [ for (final e in _todos.entries) {'id': e.key, 'text': e.value}, ];
@Get('/{id}') Future<Map<String, Object?>> show(String id) async { final text = _todos[id]; if (text == null) throw NotFound('Todo "$id" not found'); return {'id': id, 'text': text}; }
@Post('/') Future<Map<String, Object?>> create(Map<String, dynamic> body) async { final text = body['text']; if (text is! String || text.isEmpty) throw BadRequest('"text" is required'); final id = '${_todos.length + 1}'; _todos[id] = text; return {'id': id, 'text': text}; }}The part line is required: it is where the generated code goes. id is bound to the {id} placeholder, body is the decoded JSON body, and @Post answers 201 by default.
4. Generate the routing code
Section titled “4. Generate the routing code”dart run build_runner build --delete-conflicting-outputsThis writes lib/controllers/todos_controller.g.dart with a TodosControllerGisilaWeb extension that adds attachToApp. While developing, keep dart run build_runner watch --delete-conflicting-outputs running in a second terminal.
5. Boot the server
Section titled “5. Boot the server”Replace bin/todo_api.dart:
import 'package:gisila/gisila.dart';import 'package:todo_api/controllers/todos_controller.dart';
Future<void> main() async { final app = GisilaApp( config: AppConfig( logLevel: LogLevel.info, maxRequestBodyBytes: 64 * 1024, healthCheckPath: '/healthz', ), );
app.registerController(attacher: TodosController().attachToApp);
await app.start(port: 8080, stopOnSignals: true);}dart run bin/todo_api.dart# 2026-10-31T09:00:00.000Z [INFO ] gisila listening on http://0.0.0.0:8080 controllers=1 middleware=06. Recommended layout
Section titled “6. Recommended layout”As the app grows, use this layout:
todo_api/├── bin/server.dart # main(): config, registration, start()├── lib/│ ├── controllers/ # @Controller classes (+ generated .g.dart)│ ├── forms/ # Form subclasses│ ├── services/ # Service subclasses│ ├── models/ # DTOs with toJson / fromJson│ └── config.dart # builds AppConfig from the environment└── test/ # GisilaTestClient testsVerify it works
Section titled “Verify it works”curl -i -X POST localhost:8080/todos -H 'content-type: application/json' -d '{"text":"buy milk"}'# HTTP/1.1 201 Created# {"id":"1","text":"buy milk"}
curl localhost:8080/todos# [{"id":"1","text":"buy milk"}]
curl -i localhost:8080/todos/9# HTTP/1.1 404 Not Found# {"error":{"status":404,"message":"Todo \"9\" not found","code":"not_found"}}
curl localhost:8080/healthz# {"status":"ok"}Every response carries an X-Request-Id header and the default security headers.
Next steps
Section titled “Next steps”- Build a CRUD API with forms, services and a database.
- Validate input with typed forms.
- Learn the request lifecycle.