batchCooking/packages/express-tools/src/async-handler.ts
Nicolas dfeb6fd1ac API: GET /planning/current (home planning + sidebar, step 1/5)
- packages/shared: PlanningView/PlanningItemView, exported.
- apps/api: planning module (service + route), mounted at /planning.
  GET /planning/current returns the authenticated user's household's
  planning covering today, or null (no error) when there isn't one yet —
  the expected state until planning creation exists.
- Tests: Mocha (apps/api/test/planning.test.ts) + Cucumber
  (features/planning.feature), same conventions as auth.
- packages/express-tools: fixed AsyncRequestHandler/wrapAsyncHandler's
  Locals generic constraint (Record<string, unknown> -> Record<string,
  any>, matching Express's own Response<ResBody, LocalsObj>) — the first
  endpoint combining requireAuth/AuthLocals with an async handler exposed
  that the stricter constraint rejected plain interfaces Response itself
  accepts fine.
- Docs: README.md ("Planning" section) + specs/backend-architecture.md.

First commit of the home-page-after-login feature (see plan discussed in
chat) — frontend layout/routing/HomePage follow in subsequent commits on
this same branch/PR.
2026-08-16 20:46:44 +02:00

44 lines
2.2 KiB
TypeScript

import type { NextFunction, Request, RequestHandler, Response } from "express";
/**
* An Express route handler whose body is `async` (returns a `Promise`).
* Only `ResBody`/`Locals` are made generic (what this codebase actually
* varies per-route) — params/request-body/query stay at Express's own
* internal defaults, same as an unparameterized `Request`.
*
* `Locals` is constrained to `Record<string, any>`, matching Express's own
* `Response<ResBody, LocalsObj>` exactly (see `@types/express-serve-static-
* core`) rather than the stricter `Record<string, unknown>`: a plain
* `interface` (e.g. `AuthLocals` in `require-auth.ts`) has no index
* signature, so under `unknown` it fails this generic's constraint even
* though it's assignable to `Response`'s own `Locals` param directly —
* `any` is what lets that structural gap close.
*/
export type AsyncRequestHandler<
ResBody = unknown,
// biome-ignore lint/suspicious/noExplicitAny: mirrors Express's own Response<ResBody, LocalsObj extends Record<string, any>> constraint (see comment above) — `unknown` here would reject plain interfaces like AuthLocals that Response itself accepts fine.
Locals extends Record<string, any> = Record<string, any>,
> = (req: Request, res: Response<ResBody, Locals>, next: NextFunction) => Promise<void>;
/**
* Wraps an async Express handler so a thrown error or rejected promise is
* forwarded to `next(err)` automatically. Without this, an unhandled
* rejection inside an `async` route handler never reaches Express's error
* middleware — every route ends up needing its own `try { ... } catch (err)
* { next(err); }` boilerplate, which this removes.
*
* @example
* router.post("/signup", wrapAsyncHandler(async (req, res) => {
* const profile = await signup(req.body);
* res.status(201).json(profile);
* }));
*/
export function wrapAsyncHandler<
ResBody = unknown,
// biome-ignore lint/suspicious/noExplicitAny: same constraint as AsyncRequestHandler above, for the same reason.
Locals extends Record<string, any> = Record<string, any>,
>(handler: AsyncRequestHandler<ResBody, Locals>): RequestHandler {
return (req, res, next) => {
handler(req, res as Response<ResBody, Locals>, next).catch(next);
};
}