Microservice-Chassis
An opinionated framework for building and running
microservices. Heavily plugin oriented, with a goal of minimizing
repitition and boilerplate in other projects.
Opinions
I) Tooling should work out of the box. You shouldn't have to spend
time figuring out: that nyc simply doesn't work with ES Modules;
that jest has trouble resolving dynamic imports with typescript; how
to get unit tests running; how to get typescript configured "just so";
how to get code coverage reports
II) Frameworks and tools should be as up-to-date with latest standards
and specs as possible.
III) Boilerplate should be concentrated rather than repeated.
IV) In the spirit of being as up-to-date as possible, If the industry
is moving to ES Modules, we should just use them exclusively, and our
tools should help make that happen.
V) Frameworks and tools should be as open to extension as possible,
and as closed to modification as possible. That is, the API we provide
should be as stable as we can make it, while allowing other packages
to extend it. In OO circles, this is called the open-closed principle.
VI) Learning curves should have minimal slope. You should be able to
use this framework without much study beyond learning typescript,
express, and reading this README.
Features
Typescript "Just So"
We provide a sensible, very modern tsconfig.json so you're not wasting
your time trying to get it working. It's in ES Module mode, transpiles
to ES2022, and has source mapping enabled for easy debugging.
npx chassis-tsc
Test Execution and Coverage Scripts
Write nodejs test modules in Typescript; we'll run them and give you
coverage reports. Want to ignore a file when calculating coverage?
Just include the comment // c8 ignore file in the file. this is a
chassis-provided extension to c8.
You can adjust coverage levels by setting c8 options in your
package.json, like this:
{
"c8": {
"lines": 80,
"functions": 90,
"branches": 90
}
}
See here for more details on c8.
See here for more details on
writing nodejs tests.
npx chassis-test
Plugins
Microservice-Chassis scans your application for plugins, and runs
those plugins, so that your application code consists entirely of
plugins. You do not need any startup boilerplate. To create a plugin,
simply write a file conforming to this filename pattern:
*.chassis-plugin.js, and make sure it exports an object named
plugin conforming to the interface
@earnest-labs/microservice-chassis/Plugin. You
can use npx chassis-new-plugin {name} to create a plugin to
start from.
Logging
Microservice-Chassis provides a Winston logger as part of the context
object provided to your plugin. See PluginContext.ts.
This logger is automatically configured to output colorized,
timestamped log messages when running locally, or a stream of JSON
objects when running in a server environment. The logic to determine
the logging environment is housed in logging.ts#chassisLoggingEnv().
It chooses the first defined environment variable in this list:
CHASSIS_LOGGING_ENV, CHASSIS_ENV, NODE_ENV. If the logging
environment is undefined, or one of development, local, or
console, logging will use the localFormat format; otherwise it
will use the cloudFormat layout.
Microservice-Chassis also integrates Morgan, which acts as Express
middleware to perform request logging. You shouldn't need to think
about this much - just be aware that it's a thing that happens.
Express
Generally, when we think of microservices, we think of REST APIs or
perhaps Graphql APIs. Microservice-Chassis provides two HTTP ports, or
in Express terminology, applications. The first, exposed as the
application member of the PluginContext object, is intended to be
publicly accessible and is visible as port 3000. The second, exposed
as the adminApplication member of the PluginContext, is intended
for you to add status, metric, or perhaps internal API endpoints, and is
visible as port 3001.
Graphql
Graphql integration is tested regularly, but is in a separate plugin,
located at http://github.com/earnest-labs/chassis-plugin-graphql/ or
npm install @earnest-labs/chassis-plugin-graphql
Getting Started
- Make sure prerequisites are installed:
npm install -g jq
brew install jq
npm init
- Install microservice-chassis
npm install --save @earnest-labs/microservice-chassis
npx chassis-start
Ctrl+C to kill
npx chassis-new-plugin sample
npx chassis-test
npx chassis-tsc
npx chassis-start
In a separate terminal:
curl http://localhost:3000/sample
{"name":"sample", "version": "1.0.0"}
Decision Record
ES Modules over CommonJS
The industry appears to be heading this way. Therefore, we will use,
exclusively, ES Modules, in an attempt to avoid having to port
everything in the future.
C8 over NYC
While the industry is heading toward ES Modules, not all of the
tooling is quite ready. In particular, NYC provides zero coverage
when you turn on "type": "module" in package.json. So until NYC
supports ES Module code coverage, we cannot use it. There are supposed
workarounds listed
here, but the
consensus on that thread is that they don't work and you should just
use c8 for the time being.
Here
is the stackoverflow thread that first tipped us off to c8's
existence.
NodeJS test runner and asserts over mocha, chai, and/or jest.
Yes, it's still experimental, but if the javascript engine provides
testing, why have an external dependency? We're hedging this bet by
using the describe/it API, as it is basically identical to mocha, from
a developer standpoint; the only difference is whether you do import {describe,it} from "node:test"
Plugin registration order over convoluted configuration mechanism
Much talk occurred on the chassis team about whether to configure
services by overlaying config files, reading from package.json, or
some other mechanism. We ultimately decided that this is something
that belongs outside of the chassis, at least for now. Instead, we
decided to make it easier to use process.env as your configuration
source of truth. In particular, we've added a field to the
plugin type, registerOrder, which will be used as
the sorting key prior to calling the plugin registration function
register(). This allows the production of plugins that do things
like load passwords from external sources into process.env before
those passwords are used by, e.g., a database resource plugin. We
believe this approach follows the open/closed principal in that we are
allowing extensions to be able to create plugins with deterministic
behavior, while not dictating what those plugins do.