A set of MakerX core NodeJS types and utilities.
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_ENVisLocalDev
indicates whether the environment islocaldev
isDev
indicates whether the environment isdev
isProduction
indicates whether the environment isproduction
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
}
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,
}),
}
}
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???
}
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')
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.
A number of authorisation functions and HttpAuthFactory
wrappers are exported:
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.
getOnBehalfOfToken
: posts an on-behalf-of auth request to a token endpoint and returns anAccessToken
responsecreateClientCredentialsAuthFactory
: callscreateOnBehalfOfAuthFactory
using anassertionToken
and returns an authorization header.
getBasicAuthHeader
: returns aBasic {value}
auth header string based on the supplied username and password.createBasicAuthFactory
: callsgetBasicAuthHeader
and returns an authorization header.