JSON API
In addition to server-rendered HTML pages, our application provides a RESTful JSON API. This is useful for building mobile apps or single-page applications (SPAs) that need to interact with the same data.
Movie endpoints
The API endpoints use the same database logic as the web handlers but return JSON responses instead of HTML.
- List
- Show
- Create
- Update
- Delete
Fetches a list of movies. We use jsonEncode to convert the model list to a JSON string.
final movies = await database.dataSource
.query<$Movie>()
.withRelation('genre')
.orderBy('createdAt', descending: true)
.get();
return _json({'movies': movies.map(_movieViewModel).toList()});
Fetches a single movie by ID.
final movieId = int.tryParse(id);
if (movieId == null) {
return _json({'error': 'Invalid id'}, status: HttpStatus.badRequest);
}
final movie = await database.dataSource
.query<$Movie>()
.withRelation('genre')
.find(movieId);
if (movie == null) {
return _json({'error': 'Movie not found'}, status: HttpStatus.notFound);
}
return _json({'movie': _movieViewModel(movie)});
Creates a new movie from a JSON payload. We parse the request body and use MovieInsertDto.
final payload = await _readJson(request);
if (payload == null) {
return _json({'error': 'Invalid JSON'}, status: HttpStatus.badRequest);
}
final errors = _validateMoviePayload(payload);
if (errors.isNotEmpty) {
return _json({'errors': errors}, status: HttpStatus.badRequest);
}
final repo = database.dataSource.repo<$Movie>();
final movie = await repo.insert(
MovieInsertDto(
title: payload['title']!.trim(),
releaseYear: payload['releaseYear']!,
summary: _nullIfEmpty(payload['summary']),
posterPath: _nullIfEmpty(payload['posterPath']),
genreId: _parseOptionalInt(payload['genreId']),
),
);
return _json({'movie': _movieViewModel(movie)}, status: HttpStatus.created);
Updates an existing movie using a JSON payload and MovieUpdateDto.
final movieId = int.tryParse(id);
if (movieId == null) {
return _json({'error': 'Invalid id'}, status: HttpStatus.badRequest);
}
final payload = await _readJson(request);
if (payload == null) {
return _json({'error': 'Invalid JSON'}, status: HttpStatus.badRequest);
}
final repo = database.dataSource.repo<$Movie>();
final update = MovieUpdateDto(
title: payload['title'],
releaseYear: payload['releaseYear'],
summary: _nullIfEmpty(payload['summary']),
posterPath: _nullIfEmpty(payload['posterPath']),
genreId: _parseOptionalInt(payload['genreId']),
);
final movie = await repo.update(update, where: {'id': movieId});
return _json({'movie': _movieViewModel(movie)});
Deletes a movie by ID.
final movieId = int.tryParse(id);
if (movieId == null) {
return _json({'error': 'Invalid id'}, status: HttpStatus.badRequest);
}
final repo = database.dataSource.repo<$Movie>();
final deleted = await repo.deleteById(movieId);
if (deleted == 0) {
return _json({'error': 'Movie not found'}, status: HttpStatus.notFound);
}
return _json({'deleted': true, 'id': movieId});
Genre endpoints
- List
- Show
final genres = await database.dataSource
.query<$Genre>()
.orderBy('name')
.get();
return _json({'genres': genres.map(_genreViewModel).toList()});
final genreId = int.tryParse(id);
if (genreId == null) {
return _json({'error': 'Invalid id'}, status: HttpStatus.badRequest);
}
final genre = await database.dataSource.query<$Genre>().find(genreId);
if (genre == null) {
return _json({'error': 'Genre not found'}, status: HttpStatus.notFound);
}
final movies = await database.dataSource
.query<$Movie>()
.withRelation('genre')
.where('genreId', genreId)
.orderBy('createdAt', descending: true)
.get();
return _json({
'genre': _genreViewModel(genre),
'movies': movies.map(_movieViewModel).toList(),
});
Payload Format
All API payloads use the field names defined in your models. For example, when creating a movie, the JSON should look like this:
{
"title": "Inception",
"releaseYear": 2010,
"summary": "A thief who steals corporate secrets through the use of dream-sharing technology.",
"genreId": 1
}