Web Server #
Dart is a highly capable server-side language — one language for the frontend (Flutter web) and the backend (Dart server). dart:io provides a low-level HttpServer that gives full control, while the shelf package provides a more ergonomic middleware abstraction. This article covers both: from building a server from scratch with HttpServer to understand the fundamentals, to using shelf for higher productivity in building real REST APIs.
HttpServer — the Foundation of Dart Servers
#
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final server = await HttpServer.bind(
InternetAddress.anyIPv4,
8080,
shared: true, // allow rebinding after a fast restart
);
print('Server running at http://localhost:${server.port}');
// Graceful shutdown on SIGINT (Ctrl+C)
ProcessSignal.sigint.watch().listen((_) async {
print('\nShutting down the server...');
await server.close(force: false); // wait for active requests to finish
exit(0);
});
await for (final request in server) {
_tanganiRequest(request); // without await — non-blocking per request
}
}
Future<void> _tanganiRequest(HttpRequest request) async {
try {
// Set CORS headers for every response
request.response.headers
..set('Access-Control-Allow-Origin', '*')
..set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
..set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle the preflight OPTIONS
if (request.method == 'OPTIONS') {
request.response.statusCode = HttpStatus.ok;
await request.response.close();
return;
}
await _router(request);
} catch (e, stackTrace) {
print('Error: $e\n$stackTrace');
await _kirimJson(request.response, HttpStatus.internalServerError,
{'error': 'Internal Server Error'});
}
}
Routing — Mapping URLs to Handlers #
A good router separates HTTP methods and paths, supports path parameters, and returns 404 for routes not found:
import 'dart:io';
typedef Handler = Future<void> Function(HttpRequest request);
class Router {
// Map from 'METHOD /path' to a handler
final Map<String, Handler> _routes = {};
void get(String path, Handler handler) =>
_routes['GET $path'] = handler;
void post(String path, Handler handler) =>
_routes['POST $path'] = handler;
void put(String path, Handler handler) =>
_routes['PUT $path'] = handler;
void delete(String path, Handler handler) =>
_routes['DELETE $path'] = handler;
Future<void> tangani(HttpRequest request) async {
final kunci = '${request.method} ${request.uri.path}';
// Check for an exact match
final handler = _routes[kunci];
if (handler != null) {
await handler(request);
return;
}
// Check path parameters — e.g. /pengguna/:id
for (final entry in _routes.entries) {
final paramMap = _cocokkanPath(entry.key, request.method, request.uri.path);
if (paramMap != null) {
// Store path params in URI attributes
request.uri.queryParameters; // access params via _extractParams
await entry.value(request);
return;
}
}
// 404 if nothing matches
await _kirimJson(request.response, HttpStatus.notFound,
{'error': 'Route not found: ${request.uri.path}'});
}
Map<String, String>? _cocokkanPath(
String kunciRoute, String metode, String path) {
final bagianRoute = kunciRoute.split(' ');
if (bagianRoute[0] != metode) return null;
final segmenRoute = bagianRoute[1].split('/');
final segmenPath = path.split('/');
if (segmenRoute.length != segmenPath.length) return null;
final params = <String, String>{};
for (int i = 0; i < segmenRoute.length; i++) {
if (segmenRoute[i].startsWith(':')) {
params[segmenRoute[i].substring(1)] = segmenPath[i];
} else if (segmenRoute[i] != segmenPath[i]) {
return null;
}
}
return params;
}
}
Using the Router #
import 'dart:io';
import 'dart:convert';
Future<void> main() async {
final router = Router()
..get('/', _halamanUtama)
..get('/api/pengguna', _daftarPengguna)
..get('/api/pengguna/:id', _detailPengguna)
..post('/api/pengguna', _buatPengguna)
..put('/api/pengguna/:id', _updatePengguna)
..delete('/api/pengguna/:id', _hapusPengguna);
final server = await HttpServer.bind(InternetAddress.anyIPv4, 8080);
await for (final request in server) {
router.tangani(request).catchError((e) {
print('Unhandled error: $e');
});
}
}
Future<void> _halamanUtama(HttpRequest request) async {
request.response
..headers.contentType = ContentType.html
..write('<h1>Dart Web Server</h1>')
..close();
}
Future<void> _daftarPengguna(HttpRequest request) async {
// Read query parameters
final halaman = int.tryParse(request.uri.queryParameters['halaman'] ?? '1') ?? 1;
final perHalaman = int.tryParse(request.uri.queryParameters['perHalaman'] ?? '10') ?? 10;
final pengguna = await _repository.ambilSemua(halaman: halaman, perHalaman: perHalaman);
await _kirimJson(request.response, HttpStatus.ok, {
'data': pengguna.map((p) => p.toJson()).toList(),
'halaman': halaman,
'perHalaman': perHalaman,
});
}
Reading and Writing JSON #
import 'dart:io';
import 'dart:convert';
// Read the JSON body from a request
Future<Map<String, dynamic>?> bacaJson(HttpRequest request) async {
try {
final contentType = request.headers.contentType;
if (contentType?.mimeType != 'application/json') {
return null;
}
final body = await utf8.decoder.bind(request).join();
return jsonDecode(body) as Map<String, dynamic>;
} on FormatException {
return null;
}
}
// Send a JSON response
Future<void> _kirimJson(
HttpResponse response,
int statusCode,
Object data,
) async {
response
..statusCode = statusCode
..headers.contentType = ContentType.json
..write(jsonEncode(data));
await response.close();
}
// Example POST endpoint that reads and validates JSON
Future<void> _buatPengguna(HttpRequest request) async {
final body = await bacaJson(request);
if (body == null) {
await _kirimJson(request.response, HttpStatus.badRequest,
{'error': 'Body must be valid JSON'});
return;
}
final nama = body['nama'] as String?;
final email = body['email'] as String?;
if (nama == null || nama.isEmpty) {
await _kirimJson(request.response, HttpStatus.unprocessableEntity,
{'error': 'Field "nama" is required'});
return;
}
if (email == null || !email.contains('@')) {
await _kirimJson(request.response, HttpStatus.unprocessableEntity,
{'error': 'Field "email" is invalid'});
return;
}
final pengguna = await _repository.buat(nama: nama, email: email);
await _kirimJson(request.response, HttpStatus.created, pengguna.toJson());
}
The Middleware Pattern — Request Pipelines #
Middleware is a function that wraps a handler and can do something before or after the handler runs — logging, authentication, rate limiting, and more:
import 'dart:io';
typedef MiddlewareHandler = Future<void> Function(HttpRequest request);
typedef Middleware = MiddlewareHandler Function(MiddlewareHandler next);
// Logging middleware
Middleware loggingMiddleware() {
return (next) => (request) async {
final mulai = DateTime.now();
print('[${mulai.toIso8601String()}] ${request.method} ${request.uri.path}');
await next(request);
final durasi = DateTime.now().difference(mulai);
print('[${request.method}] ${request.uri.path} — ${durasi.inMilliseconds}ms '
'— ${request.response.statusCode}');
};
}
// Bearer token authentication middleware
Middleware authMiddleware(Future<bool> Function(String token) verifikasi) {
return (next) => (request) async {
final authHeader = request.headers.value(HttpHeaders.authorizationHeader);
if (authHeader == null || !authHeader.startsWith('Bearer ')) {
await _kirimJson(request.response, HttpStatus.unauthorized,
{'error': 'Authentication token required'});
return;
}
final token = authHeader.substring(7);
if (!await verifikasi(token)) {
await _kirimJson(request.response, HttpStatus.forbidden,
{'error': 'Token invalid or expired'});
return;
}
await next(request);
};
}
// Simple rate limiter middleware
Middleware rateLimitMiddleware({int maks = 100, Duration per = const Duration(minutes: 1)}) {
final hitungan = <String, List<DateTime>>{};
return (next) => (request) async {
final ip = request.connectionInfo?.remoteAddress.address ?? 'unknown';
final sekarang = DateTime.now();
final batas = sekarang.subtract(per);
hitungan.putIfAbsent(ip, () => []);
hitungan[ip]!.removeWhere((t) => t.isBefore(batas));
if (hitungan[ip]!.length >= maks) {
request.response.headers.set('Retry-After', per.inSeconds.toString());
await _kirimJson(request.response, HttpStatus.tooManyRequests,
{'error': 'Too many requests, try again later'});
return;
}
hitungan[ip]!.add(sekarang);
await next(request);
};
}
// Compose middleware — executes left to right
MiddlewareHandler compose(List<Middleware> middlewares, MiddlewareHandler handler) {
return middlewares.reversed.fold(handler, (next, mw) => mw(next));
}
// Usage
void main() async {
final pipeline = compose([
loggingMiddleware(),
rateLimitMiddleware(maks: 60),
authMiddleware(_verifikasiToken),
], _tanganiRequest);
final server = await HttpServer.bind(InternetAddress.anyIPv4, 8080);
await for (final request in server) {
pipeline(request).catchError((e) => print('Error: $e'));
}
}
Shelf — the Ergonomic Middleware Framework #
For larger projects, the shelf package provides a more mature, composable abstraction:
dart pub add shelf shelf_router shelf_static
import 'dart:io';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:shelf_router/shelf_router.dart';
import 'package:shelf_static/shelf_static.dart';
// Handlers in Shelf: Request → Response (pure functions)
Handler buatApp() {
final router = Router();
// API endpoints
router.get('/api/produk', _daftarProduk);
router.get('/api/produk/<id>', _detailProduk);
router.post('/api/produk', _buatProduk);
router.put('/api/produk/<id>', _updateProduk);
router.delete('/api/produk/<id>', _hapusProduk);
// Static files
final staticHandler = createStaticHandler('public', defaultDocument: 'index.html');
// Combine the router and static handler
final cascade = Cascade()
.add(router)
.add(staticHandler)
.handler;
// Middleware pipeline
return Pipeline()
.addMiddleware(logRequests()) // shelf's built-in logging
.addMiddleware(_corsMiddleware())
.addMiddleware(_authMiddleware())
.addHandler(cascade);
}
// Shelf handler — takes path parameters with <namaParam>
Future<Response> _detailProduk(Request request, String id) async {
final produk = await _repository.ambilById(id);
if (produk == null) {
return Response.notFound('{"error": "Product not found"}',
headers: {'Content-Type': 'application/json'});
}
return Response.ok(jsonEncode(produk.toJson()),
headers: {'Content-Type': 'application/json'});
}
// CORS middleware for Shelf
Middleware _corsMiddleware() {
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
return (innerHandler) => (request) async {
if (request.method == 'OPTIONS') {
return Response.ok('', headers: headers);
}
final response = await innerHandler(request);
return response.change(headers: headers);
};
}
// Auth middleware for Shelf
Middleware _authMiddleware() {
// Routes that don't need auth
const publik = {'/api/auth/login', '/api/auth/register'};
return (innerHandler) => (request) async {
if (publik.contains(request.url.path)) {
return innerHandler(request);
}
final auth = request.headers['Authorization'];
if (auth == null || !auth.startsWith('Bearer ')) {
return Response.unauthorized('{"error": "Token required"}',
headers: {'Content-Type': 'application/json'});
}
final token = auth.substring(7);
if (!await _verifikasiToken(token)) {
return Response.forbidden('{"error": "Invalid token"}',
headers: {'Content-Type': 'application/json'});
}
return innerHandler(request);
};
}
Future<void> main() async {
final handler = buatApp();
final server = await shelf_io.serve(handler, InternetAddress.anyIPv4, 8080);
print('Shelf server running at http://localhost:${server.port}');
}
Static Files with Correct Content-Types #
import 'dart:io';
// Extension-to-MIME-type map
const _mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.pdf': 'application/pdf',
};
Future<void> sajikanFile(HttpRequest request, String direktoriPublik) async {
var path = request.uri.path;
// Security: prevent path traversal
if (path.contains('..')) {
await _kirimJson(request.response, HttpStatus.forbidden,
{'error': 'Access denied'});
return;
}
if (path == '/') path = '/index.html';
// Join the public directory with the requested path
final filePath = '$direktoriPublik$path';
final file = File(filePath);
if (!await file.exists()) {
// Try serving index.html for SPA routing
final index = File('$direktoriPublik/index.html');
if (await index.exists()) {
await _sajikanFileObjek(request, index, 'text/html; charset=utf-8');
return;
}
request.response.statusCode = HttpStatus.notFound;
await request.response.close();
return;
}
final ekstensi = filePath.substring(filePath.lastIndexOf('.'));
final mimeType = _mimeTypes[ekstensi] ?? 'application/octet-stream';
await _sajikanFileObjek(request, file, mimeType);
}
Future<void> _sajikanFileObjek(
HttpRequest request, File file, String mimeType) async {
request.response.headers
..set('Content-Type', mimeType)
..set('Cache-Control', 'public, max-age=3600'); // cache for 1 hour
// Pipe the file directly to the response — efficient, no need to load into RAM first
await file.openRead().pipe(request.response);
}
HTTPS with TLS #
import 'dart:io';
Future<void> main() async {
final konteks = SecurityContext()
..useCertificateChain('certs/server.crt')
..usePrivateKey('certs/server.key');
final server = await HttpServer.bindSecure(
InternetAddress.anyIPv4,
443,
konteks,
);
print('HTTPS server running at https://localhost:443');
// Redirect HTTP to HTTPS (run a separate HTTP server on port 80)
_jalankanRedirectHTTP();
await for (final request in server) {
// Add the HSTS header
request.response.headers.set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains',
);
await _tanganiRequest(request);
}
}
void _jalankanRedirectHTTP() async {
final serverHttp = await HttpServer.bind(InternetAddress.anyIPv4, 80);
serverHttp.listen((request) {
request.response
..statusCode = HttpStatus.movedPermanently
..headers.set('Location',
'https://${request.headers.host}${request.uri}')
..close();
});
}
Comparison: dart:io HttpServer vs Shelf vs Frameworks
#
| Aspect | dart:io directly | Shelf | Frameworks (Conduit, Serverpod) |
|---|---|---|---|
| Boilerplate | A lot | Moderate | Little |
| Flexibility | Full | High | Limited by configuration |
| Middleware | Manual | ✓ composable | ✓ built-in |
| Routing | Manual | shelf_router | ✓ built-in |
| Testing | Difficult | Easy (pure functions) | Varies |
| Best for | Learning, full control | Production APIs | Large projects, ORM, auth |
USE dart:io directly when:
✓ Learning how HTTP works at a low level
✓ You need full control over every aspect of the request lifecycle
✓ Special cases not supported by frameworks
USE Shelf when:
✓ Building production REST APIs
✓ You need composable, easily testable middleware
✓ The team is familiar with the middleware concept (like Express.js)
USE frameworks (Conduit, Serverpod) when:
✓ Large projects with ORM, authentication, and database migration needs
✓ You want established conventions and structure
Summary #
await forwithout await per request — handle each request withoutawaitin the main loop so the server can accept the next request. The Dart event loop handles concurrency automatically.- The middleware pattern separates cross-cutting concerns (logging, auth, rate limiting) from business logic. Compose as a pipeline from left to right.
- Always close
request.responsewithclose()— otherwise the client will hang waiting for a response that never comes.- JSON APIs: use
Content-Type: application/jsonin response headers,jsonDecodeto read the body, andjsonEncodeto write the response.- Path traversal is a serious vulnerability when serving static files — always check whether the path contains
..before opening a file.file.openRead().pipe(request.response)for serving large files without loading them into RAM — more efficient thanreadAsBytes()then writing.- CORS — set
Access-Control-Allow-*headers and handle preflightOPTIONSrequests so web frontends can access the API.- Shelf is the recommended choice for production REST APIs — its handlers are pure functions (
Request → Response) that are easy to test without a real server.- HTTPS is mandatory in production — use
HttpServer.bindSecurewith valid certificates, add the HSTS header, and redirect all HTTP to HTTPS.- Graceful shutdown with
server.close(force: false)waits for all active requests to finish before shutting down — important for zero-downtime deployments.