## 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()`: ```dart 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: ```dart 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. **Cookies** — `req.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