Validate input
You’ll validate a sign-up endpoint: built-in field rules, custom validators, a custom field type, a database-dependent check, and query-parameter validation. Every failure produces the same 400 validation_failed envelope with one message per field.
Prerequisites
Section titled “Prerequisites”- A gisila project with a controller and a service (see Build a CRUD API).
1. Declare a form with built-in rules
Section titled “1. Declare a form with built-in rules”import 'package:gisila/gisila.dart';
class SignUpForm extends Form { final email = EmailField(name: 'email', required: true, maxLength: 254); final password = PasswordField(name: 'password', required: true, minLength: 10, maxLength: 128); final displayName = StringField(name: 'displayName', required: true, minLength: 2, maxLength: 50); final age = IntField(name: 'age', min: 13, max: 130); final acceptTerms = BoolField(name: 'acceptTerms', required: true);
@override List<FormField<Object?>> collectFields() => [email, password, displayName, age, acceptTerms];}Every field needs a name (the body key). Only declared keys are read; anything else in the body is ignored.
2. Add custom validators
Section titled “2. Add custom validators”Validators run after parsing and return an error message or null:
final displayName = StringField( name: 'displayName', required: true, minLength: 2, maxLength: 50, pattern: RegExp(r'^[\w .-]+$'), validators: [ (String v) => v.toLowerCase().contains('admin') ? 'This name is reserved' : null, ],);
final acceptTerms = BoolField( name: 'acceptTerms', required: true, validators: [(bool v) => v ? null : 'You must accept the terms'],);3. Add a custom field type
Section titled “3. Add a custom field type”For types the built-in fields don’t cover, extend FormField<T> and implement parse:
class CountryField extends FormField<String> { CountryField({required super.name, super.required});
static const _known = {'ET', 'KE', 'US', 'DE'};
@override String parse(Object? raw) { final code = raw is String ? raw.trim().toUpperCase() : null; if (code == null || !_known.contains(code)) { throw FormValidationException(errors: {name: 'Unknown country code'}); } return code; }}Add final country = CountryField(name: 'country', required: true); to the form and to collectFields().
4. Use the form in a controller
Section titled “4. Use the form in a controller”@Post('/signup')@Public()Future<User> signUp(SignUpForm form, AccountService accounts) => accounts.signUp(form);The generated code calls bindForm before your method. JSON, url-encoded and multipart bodies all work with the same form.
5. Check rules that need the database in the service
Section titled “5. Check rules that need the database in the service”Throw FormValidationException so the client sees the same shape:
class AccountService extends Service { Future<User> signUp(SignUpForm form) async { final email = form.email.value!.toLowerCase(); if (await _users.existsByEmail(email)) { throw FormValidationException(errors: {'email': 'Already registered'}); } return _users.create(email: email, password: form.password.value!, name: form.displayName.value!); }}6. Validate query parameters
Section titled “6. Validate query parameters”Query parameters are typed by their Dart type and coerced; a bad value is a 400 bad_request. Range-check them yourself, or use PageParams for paging:
@Get('/search')Future<List<User>> search(UserService users, {String? q, int? limit}) async { if (q == null || q.trim().length < 2) { throw BadRequest('q must be at least 2 characters', code: 'invalid_query'); } final page = PageParams(limit: limit, maxLimit: 50); return users.search(q.trim(), limit: page.limit);}Give optional query parameters nullable types and apply defaults in the body; a non-nullable optional parameter with a default value answers 400 when the client leaves it out.
7. Restrict content types and size per route
Section titled “7. Restrict content types and size per route”@Post('/signup', config: RouteConfig(contentTypes: {'application/json'}, maxBodyBytes: 8 * 1024))Other content types get 415, larger bodies 413.
Verify it works
Section titled “Verify it works”curl -s -X POST localhost:8080/signup -H 'content-type: application/json' \ -d '{"email":"bad","password":"short","displayName":"x","acceptTerms":false}'{"error":{"status":400,"code":"validation_failed","message":"Invalid form data","details":{ "email":"Does not match the expected pattern", "password":"Must be at least 10 characters", "displayName":"Must be at least 2 characters", "acceptTerms":"You must accept the terms", "country":"This field is required"}}}In a test:
final res = await client.post('/signup', json: {'email': 'bad'});expect(res.statusCode, 400);expect(res.errorCode, 'validation_failed');Next steps
Section titled “Next steps”- Forms reference for every field type and its error messages.
- Handle file uploads with
FileField. - Handle errors.