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.
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 is short for dox serve.
It runs the code generator and the server.
It restarts the server when you save a .dart file.
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:
Add the columns to the migration:
await schema.create('posts', (Table table) {
table.id();
table.string('title');
table.text('body');
table.timestamps();
});
Add the same fields to the model:
@DoxModel(table: 'posts')
class Post extends PostGenerator {
@Column()
String? title;
@Column()
String? body;
}
Then run the migration:
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
A controller method can return a model or a list of models. Dox sends it as JSON.
@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:
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
A DTO is a class with rules on its fields:
@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:
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.
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>>());
});
}
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 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.
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
422answer 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.