Skip to content

Use with the gisila framework

You’ll connect a gisila web app to PostgreSQL through gisila_orm, query from a service, turn constraint violations into proper HTTP errors, and expose a health endpoint.

bin/server.dart:

import 'package:gisila/gisila.dart';
import 'package:gisila_orm/gisila.dart';
import 'package:gisila_orm/web.dart';
Future<void> main() async {
final db = await Database.connect(
await DatabaseConfig.fromEnvironment(),
onQuery: slowQueryLogger(const Duration(milliseconds: 200), print),
);
await db.ping(); // fail at startup, not on the first request
final orm = GisilaOrmDatabaseProvider(db);
final app = GisilaApp(
config: AppConfig(
database: orm,
dbErrorMapper: const PostgresDbErrorMapper(),
healthCheckPath: '/healthz',
healthCheck: orm.isHealthy,
),
);
app.registerService<AuthorService>(AuthorService.new);
app.registerController(attacher: AuthorsController().attachToApp);
await app.start(host: '0.0.0.0', port: 8080);
}
Setting Effect
database: orm Each request can read the shared Database with ctx.db<Database>(). The pool closes when the app stops.
dbErrorMapper Uncaught constraint violations become 409 / 400 responses instead of 500s.
healthCheck: orm.isHealthy /healthz answers 503 while SELECT 1 fails.
import 'package:blog/models/blog.gisila.g.dart';
import 'package:gisila/gisila.dart';
import 'package:gisila_orm/gisila.dart';
class AuthorService extends Service {
DbContext get _db => db<Database>().context();
Future<List<Author>> list({int limit = 50, int? after}) => authors()
.after(AuthorTable.id, after)
.limit(limit.clamp(1, 100))
.all(_db);
Future<Author?> byId(int id) => authors().where(AuthorTable.id.eq(id)).first(_db);
Future<Author> create(String name, String email) =>
authors().insert({'name': name, 'email': email}).one(_db);
Future<Post> createWithFirstPost(String name, String email, String title) =>
db<Database>().transaction((tx) async {
final author = await authors().insert({'name': name, 'email': email}).one(tx);
return posts().insert({'title': title, 'author_id': author.id}).one(tx);
});
}

The provider doesn’t open a transaction per request. Open one in the service method that needs it, as createWithFirstPost does.

import 'package:gisila/gisila.dart';
part 'authors_controller.g.dart';
@Controller('/authors')
class AuthorsController {
@Get('/')
Future<List<Map<String, Object?>>> list(AuthorService authors, {int? after}) async =>
[for (final a in await authors.list(after: after)) a.toJson(exclude: ['email'])];
@Post('/')
Future<Map<String, Object?>> create(CreateAuthorForm form, AuthorService authors) async =>
(await authors.create(form.name.value!, form.email.value!)).toJson();
}

CreateAuthorForm stands for a gisila form class with name and email fields. Return toJson() maps (with exclude / only to control fields) rather than models, so the response shape is explicit. See the framework docs for forms, validation and code generation.

Creating an author with an email that exists raises PostgresUniqueViolationException. With PostgresDbErrorMapper the client gets:

{
"error": {
"status": 409,
"message": "\"email\" is already taken.",
"code": "unique_violation",
"details": { "fields": ["email"] }
}
}

with HTTP status 409. The envelope is gisila’s standard error format; the mapper supplies the status, message, code and details. The submitted value is never echoed back. Catch the exception in the service when you want a different message.

Terminal window
dart run bin/server.dart &
curl -s localhost:8080/healthz
curl -s -X POST localhost:8080/authors -H 'content-type: application/json' \
-d '{"name":"Ada","email":"ada@example.com"}'
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8080/authors \
-H 'content-type: application/json' -d '{"name":"Ada","email":"ada@example.com"}'
# 409