A HTTP router based on your Swagger/OpenAPI definition.
Usage no npm install needed!
<script type="module">
import swaggerHttpRouter from 'https://cdn.skypack.dev/swagger-http-router';
</script>
README
swagger-http-router
A HTTP router based on your Swagger/OpenAPI definition.
Why write code when you have a Swagger/OpenAPI definition?
By taking part of the Swagger/OpenAPI standard and
dependency injection patterns, swagger-http-router
provides a convenient, highly modular and easily
testable REST tool.
Usage
import { constant } from 'knifecycle';
import initDB from './services/db';
import {
initWepApplication
} from 'swagger-http-router';
import API from './swagger.api.json';
import * as HANDLERS from './handlers';
run();
async function run() {
try {
// STEP 1: Spawn a Knifecycle instance and attach
// it the API definition and its handlers
const $ = initWepApplication(API, HANDLERS);
// STEP 2: Register additional services
// Override the build in `uniqueId` service
// with the UUID v4 function
$.register(constant('uniqueId', uuid.v4))
// Provide the process environment
.register(constant('ENV', process.env))
// Register the database initializer
.register(initDB);
// STEP 3: Run your app!
// Run the execution silo that encapsulates the app
// Note that the `httpServer` and `process` services
// are injected for their respective side effects:
// creating the server and managing the process
// lifecycle
const { ENV, log, $destroy } = await $.run(['ENV', 'log', 'httpServer', 'process', '$destroy']);
log('info', `On air 🚀🌕`);
if(ENV.DRY_RUN) {
await $destroy();
}
} catch(err) {
console.error('💀 - Cannot launch the process:', err.stack);
process.exit(1);
}
)
In order to work, your Swagger definition endpoints
must provide an
operationId.
This is how the router figures out which handler
to run. Those ids have to be unique. Here is
a sample Swagger definition you could use as is:
To bring to the router the logic that each
endpoint implements, you have to create
handlers for each operationId:
// file: ./handlers.js
// Knifecycle is the dependency injection tool
// we use. It provides decorators to declare
// which dependencies to inject in your handlers
import { initializer } from 'knifecycle/dist/util';
export default initializer(
{
name: 'getUser',
type: 'service',
inject: ['db'],
},
getUser
);
async function getUser({ db }) {
return async ({ userId }) => {
const user = await db.query('users', {
id: userId,
});
return {
status: 200,
headers: {},
body: {
id: userId,
name: user.name,
}
};
}
}
As you can see, handlers are just asynchronous functions
that takes the request parameters in input and provide
a JSON serializable response in output.
This router is designed to be used with a DI system and
is particularly useful with
knifecycle.
That said, you could completely avoid using a DI system
by simply using the initializers as functions and handle
their initialization manually. See this
alternate example.
Goal
This router is just my way to do things. It is nice
if you use it and I'd be happy if you improve it.
To be honest, I think this is a better approach but I do
not want to spend energy and fight with giants to make
this a standard approach. It means that it will probably
never be the next hype and if you use it you must feel
confident with forking and maintaining it yourself.
That said, the code is well documented and not that hard.
Also, the handlers you will end with will be easily
reusable in any framework with little to no changes.
You may wonder why I found that I'd better write
my own router instead of using current solutions
like ExpressJS or HapiJS:
I want documentation first APIs. No documentation, no
web service.
I want my code to be clear and descriptive instead of
binded to some cryptic middleware or plugin defined
elsewhere. Here are some
thoughts on middlewares
that explain this statement in more depth.
I want easily testable and reusable handlers just
returning plain JSON. To be able to reuse it in
multiple situations: a lambda/serverless back-end,
when rendering server side React views or in my
GraphQL server resolvers.
I prefer functional programming: it just makes my code
better. There are too many encapsulated states in existing
frameworks. I just want my handlers to be pure and
composable. For example, why adding a CORS middleware or
plugin when you can just compose handlers?
import { reuseSpecialProps } from 'knifecycle/dist/util';
const CORS = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Keep-Alive,User-Agent',
};
export function wrapWithCORS(initHandler) {
// `reuseSpecialProps` create a new initializer
// with the original initializer properties
// applyed on it.
return reuseSpecialProps(
initHandler,
initHandlerWithCORS.bind(null, initHandler)
);
}
// This function is the actual initializer that
// wraps the handler initializer. It is executed
// once at startup.
async function initHandlerWithCORS(initHandler, services) => {
const handler = await initHandler(services);
return handleWithCors.bind(null, handler);
}
// And finally this one applies CORS to the
// response
async function handleWithCors(handler, parameters) {
const response = await handler(parameters);
return {
...response,
headers: {
...response.headers,
...CORS,
}
};
}
and finally, I want to be able to instrument my code
without having to do ugly hacks. This is why DI and
Inversion of Control are at the core of my way to
handle web services.
You may want to have a look at the
architecture notes of this module
to better grasp how it is built.
Recommendations
The above usage section shows you a very basic
usage of this router. For larger apps:
you may want to build you Swagger file to avoid
repeating yourself. It won't change anything for
swagger-http-router since it just assumes a
Swagger file.
you will probably end up by automating the
handlers loading with a configuration file.
At that point, the DI system will become very
handy.
you will certainly need some more services
to make your app work. Please double check if
one exists before creating your own. Also,
handlers can be reused so feel free to
publish yours and add your Swagger path
objects to them in order for your users to
add them to their own Swagger build.