Routes
Routes are the main source in packages which depend on @saflib/express
. They are strongly typed, using types generated by separate spec
packages which depend on @saflib/openapi
.
For clarity, a route is the specification. The handler (or "route handler") is the implementation.
File Organization
Route handlers should be organized in directories by their route prefix. For example identity route handlers are divided into users
and auth
folders based on their OpenAPI-specified paths.
Each handler (e.g. get, list, create, update, delete) should reside in its own file per best practice.
An index.ts
file within each domain directory aggregates the individual handlers into an Express Router. This router should include a response from createScopedMiddleware
, scoped to the route prefix. The router should handle the entire route path and be able to slot into an Express app without any further path qualification.
TODO: Update index routers to actually handle the entire route path.
Typing the Interface
Spec packages provide types for route requests and responses. Every handler must use these types to highlight as quickly as possible when the implementation does not match the specification.
Spec libraries export utility types with API route operationIds and status codes as keys for easy access. It is up to the handler to use and enforce those types.
Examples:
TODO: Make sure all identity routes properly type their requests, responses.
Wrapping the Handler
Each handler should be wrapped with createHandler
. It just promisify's the handler, ensuring any uncaught exceptions get passed to next
. More functionality may be added there such as more advanced typing, instrumentation, so it's important to have this intermediary around all handlers.
Error Handling
It is up to the handler to have somewhere in it a return for every specified HTTP response code in the spec, except for 401s because those are handled by @saflib/express
's middleware. The handler should be the only place where HTTP responses, successful or otherwise, are created or type checked. Essentially, it is the sole responsibility of the HTTP handler function to handle HTTP concerns.
There should be no try/catches in an HTTP handler. All unsafe operations made by the handler should return an error or a response, per best practices. Those errors should be mapped to the appropriate HTTP error response code and body. If any exceptions are thrown, error middleware will catch it and it will be treated appropriately as a 5xx.
Handlers may report errors in any of the following ways:
throw
an error created withhttps-errors
'screateError
function.- Call
next
with anhttp-error
error. - Respond directly (
res.status(code).json({} satisfies ResponseBodyType);
), same as for successful responses.
The vast majority of errors responses should simply be the error object specified in @saflib/openapi
. Error middleware will always respond with this structure, and so you can only use 1. and 2. if your spec adheres to the common error response object. If you need a custom error object, you will have to use option 3.
Note that message
is for debugging. The message
is not intended to be shown to end users as part of normal use; it is not localized. Instead, the frontend should use the HTTP status and, if necessary, the code
field to decide what to render to the user. The @saflib/vue-spa
package in fact logs the message and only propagates the http status and response code fields to Vue components. All SDKs should do the same.
TODO: Make sure message is actually console logged.
See for example get-profile.ts.
Business Logic
Since a route handler is primarily responsible for HTTP concerns, it should house as little business logic as possible. Instead it should call out to:
- Database packages using their provided queries
- Integration packages which interface with external dependencies
- RPC packages which communicate with other internal services
For more complex behaviors whcih do not fall into one of these, consider using one of the following:
- Finite state machines (with
@saflib/node-xstate
) for complex, asynchronous workflows - Database packages with transactions for complex query behaviors
Another special mention is transformers. Databases, 1st party services, and 3rd party services have their own models, and those need to be transformed from and into API requests and responses. These should be kept in a separate transformers/
directory at the root of the package.
TODO: Properly organize transformer logic in identity-service.
Warning: Currently, the largest gap in SAF is a job queue system. Once this is added, most requirements for delegating business logic in most applications should be met.