-
Notifications
You must be signed in to change notification settings - Fork 2
Description
Summary
dart_node_express works for simple JSON REST APIs (proven by the backend example), but is missing features required for production web servers: no cookie support (breaks session auth), no static file serving, no response streaming, and app.listen() returns void so there's no way to gracefully shut down.
What exists today and works
- Express app creation,
app.listen() - HTTP methods:
get,post,put,deleteon app/router;patchon router only - Middleware:
use(),useAt(),getWithMiddleware(), etc. - Request:
params,query,body,method,path,url,headers(headers is raw JSObject) - Response:
send(),json(),status(),redirect(),end(),jsonMap() - Error hierarchy: ValidationError (400), UnauthorizedError (401), ForbiddenError (403), NotFoundError (404), ConflictError (409), InternalError (500)
- Validation system: string, int, bool, optional, schema with combinators
asyncHandler()for async routeserrorHandler()middleware- Request context:
setContext(),getContext()
The examples/backend/ app is a real REST API with auth, CRUD, WebSockets, and 21 passing integration tests.
What's missing — grouped by priority
P0: Blocks common web server patterns
Cookies — req.cookies, res.cookie(), res.clearCookie()
Impact: Session-based authentication is impossible. Cannot set, read, or clear cookies. This blocks:
- Traditional web apps with server-side sessions
- CSRF protection tokens
- "Remember me" functionality
- Any app that needs to work without JavaScript (progressive enhancement)
Graceful shutdown — app.listen() returns void
File: packages/dart_node_express/lib/src/app.dart
Express's app.listen() returns a http.Server object. This wrapper discards it.
Impact: Cannot call server.close() to stop accepting new connections while finishing in-flight requests. In production deployments (Kubernetes, Docker, systemd), the process receives SIGTERM and must shut down gracefully. Without server.close(), in-flight requests are killed mid-response.
Fix: Return the server object (or a typed wrapper) from listen():
Result<HttpServer, String> listen(int port, [void Function()? callback])express.static() — serve static files
Impact: Cannot serve HTML, CSS, JS, images, or any static assets. Must hand-roll file serving (the backend example does this with raw fs + path interop).
Response streaming — res.write(), res.sendFile()
Impact: Cannot stream large responses, serve file downloads, or implement Server-Sent Events (SSE). Every response must fit in memory and be sent at once.
P1: Missing request/response APIs needed for real apps
Request properties
| Missing | What it does | Impact |
|---|---|---|
req.get(name) |
Read individual header by name | Must use raw JSObject for headers |
req.ip / req.ips |
Client IP address | Cannot log, rate-limit, or geo-locate clients |
req.hostname |
Host header | Cannot do virtual hosting or domain-based routing |
req.protocol / req.secure |
HTTP vs HTTPS | Cannot detect secure connections |
req.baseUrl / req.originalUrl |
Full URL info | Cannot reconstruct full request URL |
req.accepts() / req.is() |
Content negotiation | Cannot serve different formats based on Accept header |
Response helpers
| Missing | What it does | Impact |
|---|---|---|
res.type(mime) |
Set Content-Type | Cannot serve non-JSON responses properly |
res.set(header, value) |
Set response header | Cannot set CORS, Cache-Control, etc. |
res.append(header, value) |
Append to header | Cannot add multiple Set-Cookie headers |
res.sendStatus(code) |
Send status with default body | Minor convenience |
res.format(options) |
Content negotiation responses | Cannot serve HTML vs JSON based on Accept |
res.headersSent |
Check if headers already sent | No guard against double-send crashes |
res.download(path) |
File download with Content-Disposition | Cannot serve file downloads |
Missing app.patch() on app (only on router)
File: packages/dart_node_express/lib/src/app.dart
patch() is defined on Router but not on App. PATCH is a standard HTTP method used in REST APIs for partial updates.
P2: Missing middleware and configuration
Built-in middleware
Express provides these out of the box, but they're not wrapped:
express.json()— JSON body parsing (hand-rolled in the backend example asjsonParser())express.urlencoded()— form body parsingexpress.static()— static file serving (covered above)
App configuration
app.set(key, value)/app.get(key)— app settings (view engine, trust proxy, etc.)app.locals— template/view variablesapp.route(path)— route chainingapp.all(path, handler)— match all HTTP methodsapp.param(name, handler)— parameter-specific middleware
Potential bug: Error handler type loss through jsify/dartify
Files:
packages/dart_node_express/lib/src/async_handler.dart— line 15:.catchError((error) { next.callAsFunction(null, error.jsify()); })packages/dart_node_express/lib/src/error_handler.dart— receives the error viaerr.dartify()
When a Dart error (e.g., ConflictError) is jsify()'d into Express's next() and then dartify()'d back in the error handler, the sealed class hierarchy may be lost. The pattern match against AppError could fail, causing all errors to fall through to the default 500 response.
This needs verification. The backend integration tests DO pass with correct status codes, so either:
- The round-trip preserves enough type info (unlikely but possible)
- Routes handle their own error responses before the error handler
- Something else is going on
Either way, this code path is fragile and should be investigated.
Test gaps
File: packages/dart_node_express/test/express_test.dart (63 tests)
All tests are construction/registration tests — they verify that calling methods doesn't throw, but never make actual HTTP requests:
test('app.get registers route', () {
final app = createExpressApp();
// Just checks it doesn't throw — never sends an HTTP request
expect(() => app.get('/test', handler), returnsNormally);
});Zero tests for:
- Actual HTTP request/response cycle
- Middleware execution order
- Error handler catching errors and returning correct status codes
asyncHandlererror propagation- Request body parsing
- Response content types
- Query parameter parsing
- Route parameter extraction
Real integration tests exist in examples/backend/test/ (21 tests) — these make real HTTP requests and verify responses. But they are NOT part of the package test suite.
Suggested implementation order
app.listen()returns server handle — enables graceful shutdown, one function changereq.get(name)+res.set(name, value)— header access, small surface, high impact- Cookies —
req.cookies,res.cookie(),res.clearCookie() express.static()— static file servingreq.ip+req.hostname+req.protocol— request metadatares.type()+res.sendFile()+res.download()— non-JSON responsesapp.patch()— parity with router- Verify error handler round-trip — test jsify/dartify preserves error types
- Move backend integration tests into package test suite (or create equivalent)
Files to modify
packages/dart_node_express/lib/src/app.dart— return server fromlisten(), addpatch(), addset()/all()packages/dart_node_express/lib/src/request.dart— addget(),ip,hostname,protocol,cookies,accepts(),is()packages/dart_node_express/lib/src/response.dart— addset(),append(),type(),sendFile(),download(),cookie(),clearCookie(),headersSentpackages/dart_node_express/lib/src/— addstatic_middleware.dartforexpress.static()packages/dart_node_express/test/— add HTTP integration tests