Node Common
A set of MakerX core NodeJS types and utilities.
Environment
This module standardises minimal environment definitions based on the NODE_ENV environment variable.
NODE_ENV | Description |
---|---|
localdev | Indicates a local development setup, code can expect to have devDependencies installed, logging is expected to be more verbose etc |
dev | Indicates a deployed environment with non-production data and behaviour |
production | Indicates the live production environment with real data and optimised behaviour |
-
environment
returns process.env.NODE_ENV -
isLocalDev
indicates whether the environment islocaldev
-
isDev
indicates whether the environment isdev
-
isProduction
indicates whether the environment isproduction
Logger
This type can be used to optionally emit logging from packages without taking a dependency on any specific logging framework.
export type Logger = {
error(message: string, ...optionalParams: unknown[]): void
warn(message: string, ...optionalParams: unknown[]): void
info(message: string, ...optionalParams: unknown[]): void
verbose(message: string, ...optionalParams: unknown[]): void
debug(message: string, ...optionalParams: unknown[]): void
}
Example usage:
logger?.verbose('About to do something')
const result = doSomething()
logger?.info('Did something', { result })
The Logger
representation is compatible with Winston.
Or, if you want console output, you could use:
const logger: Logger = {
error: (message: string, ...params: unknown[]) => console.error
warn: (message: string, ...params: unknown[]) => console.warn
info: (message: string, ...params: unknown[]) => console.info
verbose: (message: string, ...params: unknown[]) => console.trace
debug: (message: string, ...params: unknown[]) => console.debug
}
HTTP
HttpClient
HttpClient
is a class wrapping fetch
to make calling Web API endpoints slightly easier:
- supports setting a base URL so relative paths can be used
- supports providing an function to set an authorization header on every request
- provides individual GET POST PUT PATCH DELETE methods
- provides default behaviour of reading the response body as JSON
- supports a form-urlencoded body POST via
postForm
method - extracts some response info into an thrown error for non-200 responses to make error responses visible in logs
Example:
export class ThingClient extends HttpClient<BaseContext> {
constructor(options: HttpClientOptions<BaseContext>) {
super(options)
}
public things(): Promise<Thing[]> {
return this.get<Thing[]>(`things`)
}
public thing(id: string): Promise<Thing> {
return this.get<Thing>(`things/${id}`)
}
public async createThing(thingInput: { name: string; date: Date }): Promise<Thing> {
const thing = await this.post<Thing>(`things`, { data: thingInput })
this.options.logger.info('Created a thing', { thingInput, thing })
}
}
const onBehalfOfAuthFactory: HttpAuthFactory<BaseContext> = async ({ user }) => {
const { access_token, expires_in } = await getOnBehalfOfToken({ ...oboConfig, assertionToken })
return { authorization: `Bearer ${access_token}` }
}
export const createServices = (context: BaseContext): Services => {
const httpClientOptions = {
requestContext: context,
logger: context.logger,
correlationId: context.requestInfo.correlationId,
}
return {
thingClient: new ThingClient({
...httpClientOptions,
baseUrl,
authFactory: onBehalfOfAuthFactory,
}),
}
}
makeHttpRequest
If the HttpClient
class is too opinionated for your use case, or you simply want a stateless wrapper for the fetch api with some sensible defaults; we export the function makeHttpRequest
which is what HttpClient
uses internally. This function takes care of request logging but leaves request encoding and response decoding to the consumer which offers a higher degree of flexibility.
import { makeHttpRequest } from './http'
const usersResponse = await makeHttpRequest({
url: 'https://localhost:8080/api/users',
method: 'GET',
headers: {
Authorization: 'Bearer abc123def',
},
ensureSuccessStatusCode: false,
logger: myLogger,
requestLogLovel: 'debug',
logContext: {
service: 'My Service Name',
version: '1.0.0',
},
accept: 'application/json',
fetchInit: {
redirect: 'manual',
},
})
if (usersResponse.ok) {
const users = await usersResponse.json()
} else if (usersResponse.status === 302) {
const redirectLocation = usersResponse.headers.get('location')
} else {
// Do something with error code???
}
HttpResponseError
A custom Error class which includes a responseInfo
field to make investigating HTTP errors (via logs etc) a little easier.
A static create
async factory will attempt to read the response body (as json, then text) and add it to the error.
const response = await fetch('https://broken.io/error', {
method: 'POST',
body,
})
if (!response.ok) throw await HttpResponseError.create(response, 'POST failed')
Polyfilling fetch for NodeJS v16
When using NodeJS v16 or below, you must polyfill the global fetch
function. We recommend using node-fetch as a drop-in polyfill choice. Note: Due to node 16 and earlier versions no longer being maintained, we also strongly recommend the project be updated to the current LTS version.
Authorisation
A number of authorisation functions and HttpAuthFactory
wrappers are exported:
Client-Credentials flow
-
getClientCredentialsToken
: posts a client-credentials auth request to a token endpoint and returns anAccessToken
response. -
createClientCredentialsAuthFactory
: callsgetClientCredentialsToken
and returns an authorization header, caching theAccessToken
response until it expires, when it will fetch a new token.
On-Behalf-Of flow
-
getOnBehalfOfToken
: posts an on-behalf-of auth request to a token endpoint and returns anAccessToken
response -
createClientCredentialsAuthFactory
: callscreateOnBehalfOfAuthFactory
using anassertionToken
and returns an authorization header.
Basic auth
-
getBasicAuthHeader
: returns aBasic {value}
auth header string based on the supplied username and password. -
createBasicAuthFactory
: callsgetBasicAuthHeader
and returns an authorization header.