Komapi
Komapi is an opinionated Node.js framework with official typescript support built on top of Koa.
Disclaimer: There will be breaking changes and outdated documentation during the pre-v1.0.0 cycles.
Komapi is essentially Koa+typescript with some added sugar, which means that you can use any Koa compatible middleware and use the Koa documentation as reference. Even though it is recommended to follow the conventions defined in the framework, it is entirely possible to use Komapi exactly as you would use Koa and still enjoy many of the built-in features.
Note: This documentation only contains Komapi specific functionality on top of Koa. For documentation on topics not covered here, please consult the official Koa documentation.
Documentation
Installation
Install through npm and make it a production dependency in your application.
$ npm install --save komapi
Usage
Hello World!
This will create a server listening on port 3000 and always respond with "Hello World!".
Create a file index.js
and add the following code.
; // Create appconst app = ; // Add middleware that always respond 'Hello World!' - using the built in `ctx.send()` helperapp; // Start listeningapp;
Configuration
Komapi comes with sensible defaults, but allows for customizations for a wide variety of use cases.
;;; const app = config: env: 'production' // This is super important to set to production when deployed proxy: true subdomainOffset: 3 silent: true keys: 'my-super-secret-key' instanceId: 'my-custom-instance-id' services: Account: AccountService Chat: ChatService logOptions: // This is passed throug directly to Pino. See Pino documentation for more information level: 'trace' redact: paths: 'request.header.authorization' 'request.header.cookie' censor: '[REDACTED]' logStream: // The writeable stream to output logs to; // Access current config through app.configconsole;
Context
Komapi creates a transaction context upon instantiation that is very useful for tracking context throughout the application. This context is most often used in the request-response cycle for keeping track of authentication, transaction-type and request-id in logs or even in code to make it context aware, without having to pass around a context object.
By default, Komapi creates a separate context for each request-response cycle through a custom middleware.
The context namesspace is available on the app.transactionContext
property.
Example middleware on how to access transaction context
{ // You use transactionContext.set('myVar', 'myValue') to set transaction values that should be available in other parts of your application console; // The request id is also available from the middleware request object console; // Continue return ;}
If you need to use transaction context outside of a request-response cycle (e.g. in a script), then you can run your code in app.run(() => myFunction())
.
Alternatively you can create the transaction context yourself and handle it manually. See cls-hooked for more information. The existing namespace is available in app.transactionContext
.
Example on how to utilize the transaction context outside of the request-response cycle
;; // Create app instanceconst app = config: instanceId: 'komapi-instanceid' ; // Function for logging transaction context. { // Log transaction context with app available console; // You can also log transaction context without app available - must know the instance id in advance. const transactionContext = ; console;} // Run async code with transaction contextapp;
Lifecycle
All applications have some life cycle events and it is important to be aware of what these means for your application. Most applications do some initialization before actually doing the work (e.g. serving http requests) followed by some clean up before shutting down. A typical web application do some initialization, such as establishing a database connection before accepting work (e.g. handling http requests), followed by a period of time in a running state while accepting work then stops accepting work before the connection is closed (to prevent connection leaks) and finally stop executing.
The current state is available in app.state
, and may be one of 4 different states
State | Description |
---|---|
STARTING |
Application is transitioning to STARTED state - triggered from app.start() |
STARTED |
Application is fully initialized and accepting work |
STOPPING |
Application is transitioning to STOPPED state - triggered from app.stop() |
STOPPED |
Application is in stopped state (or not started yet) |
There are 2 lifecycle handlers in Komapi - app.start()
and app.stop()
that must be called before accepting work and before termination of the application.
Komapi automatically registers the app.close()
handler on application termination events so you should normally not need to call it manually.
Even though you do not need to call the app.start()
handler manually if you only run code in app.run()
or through app.listen()
, it is highly recommended to be explicit and call it manually.
This ensures that any initialization is performed before accepting work and not as part of accepting the first unit of work.
If you do not call it manually and start your web application with app.listen()
, then app.start()
will be triggered automatically and the first request will wait for it to finish before handling the request.
This may result in very high latency on the first few requests.
Best practice examples on using lifecycle in Komapi
; // Create app instanceconst app = ; /** * Alternative 1: Web application */appstart; /** * Alternative 2: Run arbitrary code */appstart;
Error Handling
Error handling is important, but it is difficult to get it right and is often neglected. Komapi attempts to make error handling as flexible and simple as possible, but it is no magic bullet and it requires some effort from you, the developer. The main goals with the error handling functionality in Komapi is to:
- ensure application stability
- provide useful and consistent API responses - without leaking sensitive details - in case something went wrong
- simplify reporting and debugging in production through detailed error logging
- make it simple to do error handling right for the developer
Komapi uses botched under the hood. If a non-botched error is discovered, it will be wrapped in a botched error before logging or before being sent to the client. This ensures a consistent and secure interface that automatically conforms to the JSON:API spec using the built in errorHandler middleware.
If you want to provide context to error responses, e.g. set the status code, headers or set a human friendly error message, you must manually throw a botched error including this data. This might feel cumbersome at first, but this is done to prevent any leak of sensitive information and ensure consistency.
It is highly recommended to use botched for error handling in your code as well as encapsulating external errors in libraries. For more information on how to take advantage of botched errors, see botched documentation for more information.
Here is an example of how to use the error handling in a middleware:
; // Using Error objectsapp;
This will result in the following response (note that only code
and meta
properties are included in the response, while all properties are included in logs):
Services
Komapi has a concept of services
which encapsulates re-usable stateful functionality and makes it available throughout the application.
Services can be as simple or as complex as needed for the application, and can be inter-connected and context dependent.
Typically services should encapsulate models, complex logic (e.g. events, authorization and data visibility) and usage of other services so that your routes and controllers can be decoupled and as small as possible.
Common examples of services:
AccountService
: provides a simple interface forcreate
,update
,disable
,delete
,notify
,getActiveAccount
,getAccountsWithOutstandingInvoices
etc. Most of these methods involve complex logic such as sending out events to an eventbus, querying multiple services, ensure that data visibility is restricted based on the current authenticated contextChatService
: provides a simple interface forsendMessage
andcreateGroup
etc. The complexity of authorization, event handling and connecting to the data store is hidden from the consumer of the serviceEventService
: Enables other services to public (and subscribe to) events in a message bus, websockets, push notifications, redis cache or just locally in the application depending on needs.WebSocketService
: Manage websocket connections so that a single websocket connection can handle many different channels and events.DatabaseService
: Handle migrations, database connections and clean up when application shuts down.
All services must inherit from the base Komapi service (named export Service
), either directly or indirectly.
The services must also implement the service.start()
and service.stop()
methods if initialization or resource cleanup must be done on application app.start()
and app.stop()
respectively.
This is especially important for services that create connections or handle state - e.g. managing connections to databases, websockets, message queues and repopulating caches etc.
Typical use case for these handlers include setting up connections, and closing connections when application shuts down.
You can even publish events to let clients know that your application is shutting down and that they should reconnect to a different endpoint.
Services are initiated with Komapi in the options
object under options.services
;;; // Create appconst app = services: Account: AccountService Image: ImageService ; /** * Note that we wrap the code in `app.run()` to ensure that the context is preserved and lifecycle handlers are called correctly * * This is the only supported way of running arbitrary code outside of `app.listen()` scenarios */app;
Example service AccountService
;;; { // Get current authentication context - See documentation on Transaction Context for more information const auth = thisapptransactionContext; // Check transaction context whether we are allowed to create users if !auth || !authscope throw meta: scope: 'create_user' 'Valid authentication with scope "create_user" required to create new accounts!' ; // Check if first and last name is set - normally you would use schema validation to ensure these are set if !accountfirstName || !accountlastName throw 'Both firstName and lastName is required!'; return AccountModel; }
Typescript
Komapi is built with typescript and provides full support for types out of the box. There are several options for augmenting Komapi with your own types depending on your use case.
Option 1: Augmenting Komapi with your own types
This is the recommended approach, but requires that you only use a single Komapi configuration in your application. The benefits here outweighs the drawbacks for the vast majority of use cases, and enables Koa native libraries to function with Komapi without any bridging necessary. The obvious drawback here is that the typings will be globals so you cannot have different configurations of Komapi active at the same time. E.g. If you want to have different state, context or services, between different instances of Komapi, then this is not the option for you.
;; // Custom Types; /** * Globally augment Komapi with custom types according to your application */declare // Create app; // Utilize custom context in your middlewaresappWithCustomContext.use;
Option 2: Using Koa generics
Another option is to utilize the generics option in Koa to add state and augment the context. For convenience, we have added a third generic that you can use to add strong typing for your services.
The benefit of this option is that it keeps the scope of your types limited to only the specific instance of you application.
The drawback is that you need to specify types when using other external libraries - and they must support generic types - as they only assume default Koa types.
Most libraries are only made for use within Koa and you will therefore lose your instance specific typing - e.g. app.services
and app.log
.
;; // Custom Types // Create app; // Utilize custom context in your middlewaresappWithCustomContext.use;
To help with using libraries initially intended for Koa, we provide a generic type ContextBridge
that makes it a bit easier to handle typings using this approach.
;;; // Types // Init; // Routesrouter.get'/test', ; // Exports;
API
new Komapi([options])
; const app = ;
Parameters:
-
options
(object): Object with optionsconfig
(object): Core configurationenv
(string): Environment setting - it is highly recommended to set this toNODE_ENV
. Default:development
name
(string): The name of the application. Default:Komapi application
instanceId
(string): The unique identifier of this instance - useful to identify the application/service instance in heavily distributed systems. Default:process.env.HEROKU_DYNO_ID || *auto generated uuid*
serviceId
(string): The unique identifier of this instance - useful to identify the application/service type in heavily distributed systems. Default:process.env.HEROKU_APP_ID || 'komapi'
subdomainOffset
(number): Offset of .subdomains to ignore. See Koa documentation for more information. Default:2
proxy
(boolean): Trust proxy headers (includesx-request-id
andx-forwarded-for
). See Koa documentation for more information. Default:false
logOptions
(object): Options to pass down to the Pino logger instance. See Pino documentation for more informationlevel
(fatal
|error
|warn
|info
|debug
|trace
|silent
): Log level verbosity Default: process.env.LOG_LEVEL || 'info'- (...) - See Pino options for more information
logStream
(Writable): A writable stream to receive logs. Default: Pino.destination()services
(object): Object with map of string to classes that extend theService
class
Utility functions
ensureSchema(jsonSchema, data)
A json schema validator that provides detailed errors for native use in Komapi.
; const jsonSchema = $schema: 'http://json-schema.org/draft-07/schema#' additionalProperties: false required: 'firstName' 'lastName' type: 'object' properties: firstName: type: 'string' lastName: type: 'string' ;const data = firstName: 'John' lastName: 'Smith'; app;
Parameters:
jsonSchema
(object): The json schema to validate againstdata
(object): The data to validate
createEnsureSchema(jsonSchema)
Create a precompiled schema validator function.
This is a faster alternative to using the inline ensureSchema
function if you can compile the schema in advance.
; const jsonSchema = $schema: 'http://json-schema.org/draft-07/schema#' additionalProperties: false required: 'firstName' 'lastName' type: 'object' properties: firstName: type: 'string' lastName: type: 'string' ;const data = firstName: 'John' lastName: 'Smith'; // Create a validator functionconst validateData = ; app;
Parameters:
jsonSchema
(object): The json schema to validate against
Middlewares
errorHandler([options])
A simple, yet powerful error handling middleware. This takes care to serialize error responses automatically while keeping sensitive information hidden. Natively supports botched errors. This middleware is added automatically by Komapi.
Note: This middleware should be added after the requestLogger middleware, but before any other middleware.
; app;
Parameters:
options
(object): Object with optionsshowDetails
(boolean): Use the error.toJSON()
method to generate responses instead of only responding with{ id, code, status, title }
properties from the error? See botched documentation for more information. Default:true
requestLogger([options])
Log each request with details as they are processed. This middleware is added automatically by Komapi.
Note: This middleware should be added before any other middleware to ensure that all requests are logged.
; app;
Parameters:
options
(object): Object with optionslevel
(string): What log level to use for request logs? Choose betweenfatal
,error
,warn
,info
,debug
andtrace
. Default:info
healthReporter([options])
Komapi provides a useful health reporter middleware that conforms to the Health Check Response Format for HTTP API spec.
This can be used to easily add a useful and comprehensive health reporting endpoint.
By default it respects app.state
when reporting health, but can be extended to also provide health reporting for downstream components and add more complex logic (e.g. is the database up? available capacity? system load?).
This middleware must be added manually and should be added to a specific path.
The recommended path is /.well_known/_health
- but is application specific.
; router;
Parameters:
options
(object): Object with optionschecks
(function): A function that receives thectx
parameter and should return an object, or a Promise that resolves to an object with a requiredstatus
(valid values:pass
,fail
warn
) property, optionaloutput
(string) property and optionalchecks
(array) property with any valid property from the spec. See the link above.
Roadmap
- Stable version