Project Goal
Build a complete REST API backend in Dart using the shelf framework. The API manages a product catalog with full CRUD, JSON responses, validation, error handling, and request logging. This capstone project combines everything you've learned in this course.
Architecture
- Model:
Product with validation
- Repository: In-memory store with CRUD
- Controller: Route handlers
- Middleware: CORS + request logging
- Server: Shelf + shelf_router
# pubspec.yaml
dependencies:
shelf: ^1.4.0
shelf_router: ^1.1.0
Full Code
import 'dart:convert';
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_router/shelf_router.dart';
// ── Model ───────────────────────────────────────────────
class Product {
final int id;
final String name;
final double price;
final String category;
final int stock;
Product({required this.id, required this.name, required this.price,
required this.category, required this.stock});
factory Product.fromJson(Map<String, dynamic> j, {int? id}) => Product(
id: id ?? j['id'] ?? 0,
name: j['name'] as String,
price: (j['price'] as num).toDouble(),
category: j['category'] as String,
stock: j['stock'] as int? ?? 0,
);
Map<String, dynamic> toJson() => {
'id': id, 'name': name, 'price': price, 'category': category, 'stock': stock,
};
}
// ── Repository ─────────────────────────────────────────
class ProductRepository {
final Map<int, Product> _db = {};
int _nextId = 1;
List<Product> getAll({String? category}) {
var products = _db.values;
if (category != null) products = products.where((p) => p.category == category);
return products.toList()..sort((a, b) => a.id.compareTo(b.id));
}
Product? getById(int id) => _db[id];
Product create(Map<String, dynamic> data) {
var p = Product.fromJson(data, id: _nextId++);
_db[p.id] = p;
return p;
}
Product? update(int id, Map<String, dynamic> data) {
if (!_db.containsKey(id)) return null;
var updated = Product.fromJson({'id': id, ...data});
_db[id] = updated;
return updated;
}
bool delete(int id) {
if (!_db.containsKey(id)) return false;
_db.remove(id);
return true;
}
}
// ── Helpers ────────────────────────────────────────────
Response json(dynamic data, {int status = 200}) => Response(
status,
body: jsonEncode(data),
headers: {'Content-Type': 'application/json; charset=utf-8'},
);
Response error(String message, {int status = 400}) =>
json({'error': message, 'status': status}, status: status);
// ── Controller ────────────────────────────────────────
class ProductController {
final ProductRepository _repo;
ProductController(this._repo);
Response getAll(Request req) {
var cat = req.url.queryParameters['category'];
var products = _repo.getAll(category: cat);
return json({'data': products.map((p) => p.toJson()).toList(), 'count': products.length});
}
Response getById(Request req, String id) {
var product = _repo.getById(int.tryParse(id) ?? -1);
return product != null ? json(product.toJson()) : error('Product not found', status: 404);
}
Future<Response> create(Request req) async {
try {
var body = jsonDecode(await req.readAsString()) as Map<String, dynamic>;
if (body['name'] == null || body['price'] == null || body['category'] == null) {
return error('name, price, and category are required');
}
var p = _repo.create(body);
return json(p.toJson(), status: 201);
} on FormatException {
return error('Invalid JSON');
}
}
Future<Response> update(Request req, String id) async {
try {
var body = jsonDecode(await req.readAsString()) as Map<String, dynamic>;
var p = _repo.update(int.tryParse(id) ?? -1, body);
return p != null ? json(p.toJson()) : error('Product not found', status: 404);
} on FormatException {
return error('Invalid JSON');
}
}
Response delete(Request req, String id) {
return _repo.delete(int.tryParse(id) ?? -1)
? json({'message': 'Deleted', 'id': int.parse(id)})
: error('Product not found', status: 404);
}
}
// ── Server ────────────────────────────────────────────
void main() async {
var repo = ProductRepository();
// Seed data
repo.create({'name': 'Dart Programming Book', 'price': 39.99, 'category': 'books', 'stock': 50});
repo.create({'name': 'Flutter Widget Catalog', 'price': 29.99, 'category': 'books', 'stock': 30});
repo.create({'name': 'Mechanical Keyboard', 'price': 89.99, 'category': 'electronics', 'stock': 15});
var ctrl = ProductController(repo);
var router = Router()
..get('/products', ctrl.getAll)
..get('/products/<id>', ctrl.getById)
..post('/products', ctrl.create)
..put('/products/<id>', ctrl.update)
..delete('/products/<id>', ctrl.delete)
..get('/health', (req) => json({'status': 'ok', 'time': DateTime.now().toIso8601String()}));
var handler = Pipeline()
.addMiddleware(logRequests())
.addHandler(router.call);
var server = await io.serve(handler, 'localhost', 8080);
print('Product API running on http://localhost:$${server.port}');
print(' GET /products');
print(' GET /products?category=books');
print(' GET /products/1');
print(' POST /products');
print(' PUT /products/1');
print(' DELETE /products/1');
}
Testing the API
# Get all products
curl http://localhost:8080/products
# Filter by category
curl http://localhost:8080/products?category=books
# Get one product
curl http://localhost:8080/products/1
# Create a product
curl -X POST http://localhost:8080/products \
-H "Content-Type: application/json" \
-d '{"name":"USB Hub","price":24.99,"category":"electronics","stock":100}'
# Update
curl -X PUT http://localhost:8080/products/1 \
-H "Content-Type: application/json" \
-d '{"name":"Dart Book 2nd Ed","price":44.99,"category":"books","stock":40}'
# Delete
curl -X DELETE http://localhost:8080/products/4
Challenges
- Add JWT authentication middleware
- Persist products to a JSON file or SQLite database
- Add pagination (
?page=1&limit=10)
- Add input validation with a schema validator
- Write integration tests using the Dart
test package and shelf_test
- Deploy to a VPS using
dart compile exe