Skip to content

dart_node_express: No cookies, no static files, no streaming, no graceful shutdown #47

@MelbourneDeveloper

Description

@MelbourneDeveloper

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, delete on app/router; patch on 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 routes
  • errorHandler() 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 as jsonParser())
  • express.urlencoded() — form body parsing
  • express.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 variables
  • app.route(path) — route chaining
  • app.all(path, handler) — match all HTTP methods
  • app.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 via err.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:

  1. The round-trip preserves enough type info (unlikely but possible)
  2. Routes handle their own error responses before the error handler
  3. 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
  • asyncHandler error 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

  1. app.listen() returns server handle — enables graceful shutdown, one function change
  2. req.get(name) + res.set(name, value) — header access, small surface, high impact
  3. Cookiesreq.cookies, res.cookie(), res.clearCookie()
  4. express.static() — static file serving
  5. req.ip + req.hostname + req.protocol — request metadata
  6. res.type() + res.sendFile() + res.download() — non-JSON responses
  7. app.patch() — parity with router
  8. Verify error handler round-trip — test jsify/dartify preserves error types
  9. Move backend integration tests into package test suite (or create equivalent)

Files to modify

  • packages/dart_node_express/lib/src/app.dart — return server from listen(), add patch(), add set()/all()
  • packages/dart_node_express/lib/src/request.dart — add get(), ip, hostname, protocol, cookies, accepts(), is()
  • packages/dart_node_express/lib/src/response.dart — add set(), append(), type(), sendFile(), download(), cookie(), clearCookie(), headersSent
  • packages/dart_node_express/lib/src/ — add static_middleware.dart for express.static()
  • packages/dart_node_express/test/ — add HTTP integration tests

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions