Skip to content

Build a Dart API with Dox

This post shows how to go from an empty folder to a tested API with a database, validation and a native binary, in a few short steps.

Each step has a small code block and a link to its docs page. The code comes from the tutorial, which builds a small blog API.

1. Install the CLI and make a project

You need Dart 3.13 or newer.

dart pub global activate dox
dox create blog
cd blog
dart pub get

dox create downloads the starter project and makes a .env file with a new random APP_KEY. The starter project uses SQLite, so there is no database to install.

Docs: Installation

2. Start the server

dox s

dox s is short for dox serve. It runs the code generator and the server. It restarts the server when you save a .dart file.

curl http://localhost:3000/api/ping
pong

Docs: Step 1: Create the project

3. Make a model and a migration

Stop dox s first with Ctrl+C. dox s restarts the server when a new file appears, and the server runs new migrations at start. So it would run the new migration before you add the columns.

One command makes the model and the migration:

dox make:model Post -m

Add the columns to the migration:

db/migration/<date>_create_posts_table.dart
await schema.create('posts', (Table table) {
  table.id();
  table.string('title');
  table.text('body');
  table.timestamps();
});

Add the same fields to the model:

lib/app/models/post/post.model.dart
@DoxModel(table: 'posts')
class Post extends PostGenerator {
  @Column()
  String? title;

  @Column()
  String? body;
}

Then run the migration:

dox migration:run

Then start dox s again. The server also runs new migrations each time it starts.

Docs: Step 2: Model and migration, Model

4. Add a controller and routes

dox make:controller Post -r

A controller method can return a model or a list of models. Dox sends it as JSON.

lib/app/http/controllers/post.controller.dart
@Inject()
class PostController {
  Future<List<Post>> index(DoxRequest req) async {
    return Post().all();
  }

  Future<Post> show(int id) async {
    Post? post = await Post().find(id);
    if (post == null) {
      throw NotFoundHttpException();
    }
    return post;
  }
}

One line adds the routes:

lib/routes/api.dart
Route.resource('posts', PostController);

Route.resource makes a route for each method the controller has. Here that is GET /api/posts and GET /api/posts/{id}. Dox makes the controller for you, because it has @Inject().

Docs: Step 3: Routes and controller, Routing

5. Check the input with a DTO

dox make:dto SavePost

A DTO is a class with rules on its fields:

lib/app/dtos/save_post/save_post.dto.dart
@Dto()
class SavePost {
  @Trim()
  @Length(min: 3, max: 100)
  final String title;

  @Trim()
  @MinLength(10)
  final String body;

  const SavePost({required this.title, required this.body});

  static const DtoSchema<SavePost> schema = $SavePostDto;
}

Ask for it as a parameter in the controller:

Future<DoxResponse> store(SavePost body) async {
  Post post = Post()
    ..title = body.title
    ..body = body.body;
  await post.save();
  return response(post).statusCode(201);
}

Dox checks the body before the method runs. Your code gets a typed object. Bad input gets a 422 answer with one message for each field:

{"title":"The title must be between 3 and 100 characters","body":"The body must be at least 10 characters"}

Add two lines to lib/config/app.dart, so the app knows the DTOs:

lib/config/app.dart
import 'package:blog/generated/dtos.g.dart';

AppConfig appConfig = AppConfig(
  dtos: dtos,
  // ... the rest stays the same
);

Docs: Step 4: Validation, Validation

6. Write a test

DoxTestClient starts the app inside the test, on a free port.

test/http_test.dart
void main() {
  late DoxTestClient client;

  setUpAll(() async {
    client = await DoxTestClient.start(appConfig);
  });

  tearDownAll(() async {
    await client.close();
  });

  test('GET /api/posts gives a list', () async {
    TestResponse res = await client.get('/api/posts');
    res.expectStatus(200).expectJson(isA<List<dynamic>>());
  });
}
dart test

The full test in the tutorial also runs the migrations first, so it works on an empty database.

Docs: Step 6: Test, Testing

7. Build one binary

dox build

dox build runs the code generator, then compiles the app. The result is in build/bundle. bin/server is one native binary. It does not need the Dart SDK.

cd build/bundle
cp ../../.env .env
./bin/server

The project also has a Dockerfile that builds the same bundle into a small image.

Docs: Step 7: Build and deploy, Docker

What you have now

  • A server that restarts when you save a file.
  • A database table, a model and a migration.
  • Routes and a controller that return JSON.
  • Input checks with a typed DTO and a 422 answer for bad input.
  • A test that runs the real app.
  • One binary for production.

The tutorial does all of this in full, and adds login with dox_auth in step 5.