Security News
JSR Working Group Kicks Off with Ambitious Roadmap and Plans for Open Governance
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
mappersmith
Advanced tools
Mappersmith is a lightweight HTTP client library for JavaScript that simplifies the process of making HTTP requests. It provides a declarative way to define API endpoints and offers features like middleware support, request/response transformation, and retries.
Declarative API Definition
Mappersmith allows you to define your API endpoints in a declarative manner. This example shows how to define a client with endpoints for fetching all users and fetching a user by ID.
const { forge } = require('mappersmith');
const api = forge({
clientId: 'my-api',
host: 'https://api.example.com',
resources: {
User: {
all: { path: '/users' },
byId: { path: '/users/{id}' }
}
}
});
api.User.all().then(response => console.log(response.data));
Middleware Support
Mappersmith supports middleware, allowing you to intercept and modify requests and responses. This example demonstrates how to log requests and responses using middleware.
const { forge, configs } = require('mappersmith');
configs.middleware = [
(request) => {
console.log('Request:', request);
return request;
},
(response) => {
console.log('Response:', response);
return response;
}
];
const api = forge({
clientId: 'my-api',
host: 'https://api.example.com',
resources: {
User: {
all: { path: '/users' }
}
}
});
api.User.all().then(response => console.log(response.data));
Request/Response Transformation
Mappersmith allows you to transform requests and responses. This example shows how to transform the response to extract user names from the list of users.
const { forge } = require('mappersmith');
const api = forge({
clientId: 'my-api',
host: 'https://api.example.com',
resources: {
User: {
all: { path: '/users', transform: (response) => response.data.map(user => user.name) }
}
}
});
api.User.all().then(userNames => console.log(userNames));
Retries
Mappersmith provides built-in support for retrying failed requests. This example configures the client to retry failed requests up to 3 times with exponential backoff.
const { forge, configs } = require('mappersmith');
configs.retry = { retries: 3, factor: 2, minTimeout: 1000 };
const api = forge({
clientId: 'my-api',
host: 'https://api.example.com',
resources: {
User: {
all: { path: '/users' }
}
}
});
api.User.all().then(response => console.log(response.data));
Axios is a popular promise-based HTTP client for JavaScript. It offers a simple API for making HTTP requests and supports features like interceptors, request/response transformation, and automatic JSON parsing. Compared to Mappersmith, Axios is more widely used and has a larger community, but it does not provide a declarative way to define API endpoints.
Fetch is a built-in web API for making HTTP requests in modern browsers. It provides a low-level API for making requests and handling responses. While Fetch is more lightweight and has no external dependencies, it lacks the higher-level abstractions and features like middleware and retries that Mappersmith offers.
Superagent is a small, progressive HTTP request library for Node.js and browsers. It provides a flexible API for making HTTP requests and supports features like plugins, request/response transformation, and retries. Compared to Mappersmith, Superagent is more flexible but requires more manual setup for defining API endpoints.
Mappersmith is a lightweight rest client for node.js and the browser. It creates a client for your API, gathering all configurations into a single place, freeing your code from HTTP configurations.
npm install mappersmith --save
# yarn add mappersmith
Download the tag/latest version from the dist folder.
Install the dependencies
yarn
Build
npm run build
npm run release # for minified version
To create a client for your API you will need to provide a simple manifest. If your API reside in the same domain as your app you can skip the host
configuration. Each resource has a name and a list of methods with its definitions, like:
import forge from 'mappersmith'
const github = forge({
clientId: 'github',
host: 'https://status.github.com',
resources: {
Status: {
current: { path: '/api/status.json' },
messages: { path: '/api/messages.json' },
lastMessage: { path: '/api/last-message.json' }
}
}
})
github.Status.lastMessage().then((response) => {
console.log(`status: ${response.data()}`)
})
If you are using commonjs, your require
should look like:
const forge = require('mappersmith').default
Each resource has a name and a list of methods with its definitions. A method definition can have host, path, method, headers, params, bodyAttr, headersAttr and authAttr. Example:
const client = forge({
resources: {
User: {
all: { path: '/users' },
// {id} is a dynamic segment and will be replaced by the parameter "id"
// when called
byId: { path: '/users/{id}' },
// {group} is also a dynamic segment but it has default value "general"
byGroup: { path: '/users/groups/{group}', params: { group: 'general' } },
// {market?} is an optional dynamic segment. If called without a value
// for the "market" parameter, {market?} will be removed from the path
// including any prefixing "/".
// This example: '/{market?}/users' => '/users'
count: { path: '/{market?}/users' } }
},
Blog: {
// The HTTP method can be configured through the `method` key, and a default
// header "X-Special-Header" has been configured for this resource
create: { method: 'post', path: '/blogs', headers: { 'X-Special-Header': 'value' } },
// There are no restrictions for dynamic segments and HTTP methods
addComment: { method: 'put', path: '/blogs/{id}/comment' },
// `queryParamAlias` will map parameter names to their alias when
// constructing the query string
bySubject: { path: '/blogs', queryParamAlias: { subjectId: 'subject_id' } },
// `path` is a function to map passed params to a custom path
byDate: { path: ({date}) => `${date.getYear()}/${date.getMonth()}/${date.getDate()}` }
}
}
})
If your method doesn't require any parameter, you can just call it without them:
client.User
.all() // https://my.api.com/users
.then((response) => console.log(response.data()))
.catch((response) => console.error(response.data()))
Every parameter that doesn't match a pattern {parameter-name}
in path will be sent as part of the query string:
client.User.all({ active: true }) // https://my.api.com/users?active=true
When a method requires a parameters and the method is called without it, Mappersmith will raise an error:
client.User.byId(/* missing id */)
// throw '[Mappersmith] required parameter missing (id), "/users/{id}" cannot be resolved'
It is possible to configure default parameters for your resources, just use the key params
in the definition. It will replace params in the URL or include query strings.
If we call client.User.byGroup
without any params it will default group
to "general"
client.User.byGroup() // https://my.api.com/users/groups/general
And, of course, we can override the defaults:
client.User.byGroup({ group: 'cool' }) // https://my.api.com/users/groups/cool
Sometimes the expected format of your query parameters doesn't match that of your codebase. For example, maybe you're using camelCase
in your code but the API you are calling expects snake_case
. In that case, set queryParamAlias
in the definition to an object that describes a mapping between your input parameter and the desired output format.
This mapping will not be applied to params in the URL.
client.Blog.all({ subjectId: 10 }) // https://my.api.com/blogs?subject_id=10
To send values in the request body (usually for POST, PUT or PATCH methods) you will use the special parameter body
:
client.Blog.create({
body: {
title: 'Title',
tags: ['party', 'launch']
}
})
By default, it will create a urlencoded version of the object (title=Title&tags[]=party&tags[]=launch
). If the body used is not an object it will use the original value. If body
is not possible as a special parameter for your API you can configure it through the param bodyAttr
:
// ...
{
create: { method: 'post', path: '/blogs', bodyAttr: 'payload' }
}
// ...
client.Blog.create({
payload: {
title: 'Title',
tags: ['party', 'launch']
}
})
NOTE: It's possible to post body as JSON, check the EncodeJsonMiddleware below for more information
NOTE: The bodyAttr
param can be set at manifest level.
To define headers in the method call use the parameter headers
:
client.User.all({ headers: { Authorization: 'token 1d1435k' } })
If headers
is not possible as a special parameter for your API you can configure it through the param headersAttr
:
// ...
{
all: { path: '/users', headersAttr: 'h' }
}
// ...
client.User.all({ h: { Authorization: 'token 1d1435k' } })
NOTE: The headersAttr
param can be set at manifest level.
To define credentials for basic auth use the parameter auth
:
client.User.all({ auth: { username: 'bob', password: 'bob' } })
The available attributes are: username
and password
.
This will set an Authorization
header. This can still be overridden by custom headers.
If auth
is not possible as a special parameter for your API you can configure it through the param authAttr
:
// ...
{
all: { path: '/users', authAttr: 'secret' }
}
// ...
client.User.all({ secret: { username: 'bob', password: 'bob' } })
NOTE: A default basic auth can be configured with the use of the BasicAuthMiddleware, check the middleware section below for more information.
NOTE: The authAttr
param can be set at manifest level.
To define the number of milliseconds before the request times out use the parameter timeout
:
client.User.all({ timeout: 1000 })
If timeout
is not possible as a special parameter for your API you can configure it through the param timeoutAttr
:
// ...
{
all: { path: '/users', timeoutAttr: 'maxWait' }
}
// ...
client.User.all({ maxWait: 500 })
NOTE: A default timeout can be configured with the use of the TimeoutMiddleware, check the middleware section below for more information.
NOTE: The timeoutAttr
param can be set at manifest level.
There are some cases where a resource method resides in another host, in those cases you can use the host
key to configure a new host:
// ...
{
all: { path: '/users', host: 'http://old-api.com' }
}
// ...
client.User.all() // http://old-api.com/users
In case you need to overwrite the host for a specific call, you can do so through the param host
:
// ...
{
all: { path: '/users', host: 'http://old-api.com' }
}
// ...
client.User.all({ host: 'http://very-old-api.com' }) // http://very-old-api.com/users
If host
is not possible as a special parameter for your API, you can configure it through the param hostAttr
:
// ...
{
all: { path: '/users', hostAttr: 'baseUrl' }
}
// ...
client.User.all({ baseUrl: 'http://very-old-api.com' }) // http://very-old-api.com/users
If the data being fetched is in binary form, such as a PDF, you may add the binary
key, and set it to true. The response data will then be a Buffer in NodeJS, and a Blob in the browser.
// ...
{
report: { path: '/report.pdf', binary: true }
}
// ...
Mappersmith does not apply any polyfills, it depends on a native Promise implementation to be supported. If your environment doesn't support Promises, please apply the polyfill first. One option can be then/promises
In some cases it is not possible to use/assign the global Promise
constant, for those cases you can define the promise implementation used by Mappersmith.
For example, using the project rsvp.js (a tiny implementation of Promises/A+):
import RSVP from 'rsvp'
import { configs } from 'mappersmith'
configs.Promise = RSVP.Promise
All Promise
references in Mappersmith use configs.Promise
. The default value is the global Promise.
Mappersmith will provide an instance of its own Response
object to the promises. This object has the methods:
request()
- Returns the original Requeststatus()
- Returns the status numbersuccess()
- Returns true for status greater than 200 and lower than 400headers()
- Returns an object with all headers, keys in lower caseheader(name)
- Returns the value of the headerdata()
- Returns the response data, if Content-Type
is application/json
it parses the response and returns an objecterror()
- Returns the last error instance that caused the request to fail or null
The behavior between your client and the API can be customized with middleware. A middleware is a function which returns an object with two methods: request and response.
The prepareRequest
method receives a function which returns a Promise
resolving the Request. This function must return a Promise
resolving the request. The method enhance
can be used to generate a new request based on the previous one.
The response
method receives a function which returns a Promise
resolving the Response. This function must return a Promise
resolving the Response. The method enhance
can be used to generate a new response based on the previous one.
const MyMiddleware = () => ({
prepareRequest(next) {
return next().then(request => request.enhance({
headers: { 'x-special-request': '->' }
}))
},
response(next) {
return next().then((response) => response.enhance({
headers: { 'x-special-response': '<-' }
}))
}
})
Sometimes you may need to set data to be available to all your client's middleware. In this
case you can use the setContext
helper, like so:
import { setContext } from 'mappersmith'
const MyMiddleware = ({ context }) => ({
/* ... */
})
setContext({ some: 'data'})
client.User.all()
// context: { some: 'data' }
This is specially useful when using mappermith coupled with back-end services. For instance you could define a globally available correlation id middleware like this:
import forge, { configs, setContext } from 'mappersmith'
import express from 'express'
const CorrelationIdMiddleware = ({ context }) => ({
request(request) {
return request.enhance({
headers: {
'correlation-id': context.correlationId
}
})
}
})
configs.middleware = [CorrelationIdMiddleware]
const api = forge({ ... })
const app = express()
app.use((req, res, next) => {
setContext({
correlationId: req.headers['correlation-id']
})
})
// Then, when calling `api.User.all()` in any handler it will include the
// `correlation-id` header automatically.
Note that setContext will merge the object provided with the current context instead of replacing it.
It can, optionally, receive resourceName
, resourceMethod
, #context, clientId
and mockRequest
. Example:
const MyMiddleware = ({ resourceName, resourceMethod, context, clientId, mockRequest }) => ({
/* ... */
})
client.User.all()
// resourceName: 'User'
// resourceMethod: 'all'
// clientId: 'myClient'
// context: {}
// mockRequest: false
Before mocked clients can assert whether or not their mock definition matches a request they have to execute their middleware on that request. This means that middleware might be executed multiple times for the same request. More specifically, the middleware will be executed once per mocked client that utilises the middleware until a mocked client with a matching definition is found. If you want to avoid middleware from being called multiple times you can use the optional "mockRequest" boolean flag. The value of this flag will be truthy whenever the middleware is being executed during the mock definition matching phase. Otherwise its value will be falsy. Example:
const MyMiddleware = ({ mockRequest }) => {
prepareRequest(next) {
if (mockRequest) {
... // executed once for each mocked client that utilises the middleware
}
if (!mockRequest) {
... // executed once for the matching mock definition
}
return next().then(request => request)
}
}
The prepareRequest
phase can optionally receive a function called "abort". This function can be used to abort the middleware execution early-on and throw a custom error to the user. Example:
const MyMiddleware = () => {
prepareRequest(next, abort) {
return next().then(request =>
request.header('x-special')
? response
: abort(new Error('"x-special" must be set!'))
)
}
}
The response
phase can optionally receive a function called "renew". This function can be used to rerun the middleware stack. This feature is useful in some scenarios, for example, automatically refreshing an expired access token. Example:
const AccessTokenMiddleware = () => {
// maybe this is stored elsewhere, here for simplicity
let accessToken = null
return () => ({
request(request) {
return Promise
.resolve(accessToken)
.then((token) => token || fetchAccessToken())
.then((token) => {
accessToken = token
return request.enhance({
headers: { 'Authorization': `Token ${token}` }
})
})
},
response(next, renew) {
return next().catch(response => {
if (response.status() === 401) { // token expired
accessToken = null
return renew()
}
return next()
})
}
})
}
Then:
const AccessToken = AccessTokenMiddleware()
const client = forge({
// ...
middleware: [ AccessToken ],
// ...
})
"renew" can only be invoked sometimes before it's considered an infinite loop, make sure your middleware can distinguish an error from a "renew". By default, mappersmith will allow 2 calls to "renew". This can be configured with configs.maxMiddlewareStackExecutionAllowed
. It's advised to keep this number low. Example:
import { configs } from 'mappersmith'
configs.maxMiddlewareStackExecutionAllowed = 3
If an infinite loop is detected, mappersmith will throw an error.
Middleware scope can be Global, Client or on Resource level. The order will be applied in this order: Resource level applies first, then Client level, and finally Global level. The subsections below describes the differences and how to use them correctly.
Resource middleware are configured using the key middleware
in the resource level of manifest, example:
const client = forge({
clientId: 'myClient',
resources: {
User: {
all: {
// only the `all` resource will include MyMiddleware:
middleware: [ MyMiddleware ],
path: '/users'
}
}
}
})
Client middleware are configured using the key middleware
in the root level of manifest, example:
const client = forge({
clientId: 'myClient',
// all resources in this client will include MyMiddleware:
middleware: [ MyMiddleware ],
resources: {
User: {
all: { path: '/users' }
}
}
})
Global middleware are configured on a config level, and all new clients will automatically include the defined middleware, example:
import forge, { configs } from 'mappersmith'
configs.middleware = [MyMiddleware]
// all clients defined from now on will include MyMiddleware
ignoreGlobalMiddleware
, e.g:forge({
ignoreGlobalMiddleware: true,
// + the usual configurations
})
Automatically configure your requests with basic auth
import BasicAuthMiddleware from 'mappersmith/middleware/basic-auth'
const BasicAuth = BasicAuthMiddleware({ username: 'bob', password: 'bob' })
const client = forge({
middleware: [ BasicAuth ],
/* ... */
})
client.User.all()
// => header: "Authorization: Basic Ym9iOmJvYg=="
** The default auth can be overridden with the explicit use of the auth parameter, example:
client.User.all({ auth: { username: 'bill', password: 'bill' } })
// auth will be { username: 'bill', password: 'bill' } instead of { username: 'bob', password: 'bob' }
Automatically configure your requests by adding a header with the value of a cookie - If it exists. The name of the cookie (defaults to "csrfToken") and the header (defaults to "x-csrf-token") can be set as following;
import CSRF from 'mappersmith/middleware/csrf'
const client = forge({
middleware: [ CSRF('csrfToken', 'x-csrf-token') ],
/* ... */
})
client.User.all()
Automatically adds X-Started-At
, X-Ended-At
and X-Duration
headers to the response.
import Duration from 'mappersmith/middleware/duration'
const client = forge({
middleware: [ Duration ],
/* ... */
})
client.User.all({ body: { name: 'bob' } })
// => headers: "X-Started-At=1492529128453;X-Ended-At=1492529128473;X-Duration=20"
Automatically encode your objects into JSON
import EncodeJson from 'mappersmith/middleware/encode-json'
const client = forge({
middleware: [ EncodeJson ],
/* ... */
})
client.User.all({ body: { name: 'bob' } })
// => body: {"name":"bob"}
// => header: "Content-Type=application/json;charset=utf-8"
Provides a catch-all function for all requests. If the catch-all function returns true
it prevents the original promise to continue.
import GlobalErrorHandler, { setErrorHandler } from 'mappersmith/middleware/global-error-handler'
setErrorHandler((response) => {
console.log('global error handler')
return response.status() === 500
})
const client = forge({
middleware: [ GlobalErrorHandler ],
/* ... */
})
client.User
.all()
.catch((response) => console.error('my error'))
// If status != 500
// output:
// -> global error handler
// -> my error
// IF status == 500
// output:
// -> global error handler
Log all requests and responses. Might be useful in development mode.
import Log from 'mappersmith/middleware/log'
const client = forge({
middleware: [ Log ],
/* ... */
})
This middleware will automatically retry GET requests up to the configured amount of retries using a randomization function that grows exponentially. The retry count and the time used will be included as a header in the response. By default on requests with response statuses >= 500 will be retried.
It's possible to configure the header names and parameters used in the calculation by providing a configuration object when creating the middleware.
If no configuration is passed when creating the middleware then the defaults will be used.
import Retry from 'mappersmith/middleware/retry/v2'
const retryConfigs = {
headerRetryCount: 'X-Mappersmith-Retry-Count',
headerRetryTime: 'X-Mappersmith-Retry-Time',
maxRetryTimeInSecs: 5,
initialRetryTimeInSecs: 0.1,
factor: 0.2, // randomization factor
multiplier: 2, // exponential factor
retries: 5, // max retries
validateRetry: (response) => response.responseStatus >= 500 // a function that returns true if the request should be retried
}
const client = forge({
middleware: [ Retry(retryConfigs) ],
/* ... */
})
The v1 retry middleware is now deprecated as it relies on global configuration which can cause issues if you need to have multiple different configurations.
import Retry from 'mappersmith/middleware/retry'
const client = forge({
middleware: [ Retry ],
/* ... */
})
It's possible to configure the header names and parameters used in the calculation by calling the deprecated setRetryConfigs method.
import { setRetryConfigs } from 'mappersmith/middleware/retry'
// Using the default values as an example
setRetryConfigs({
headerRetryCount: 'X-Mappersmith-Retry-Count',
headerRetryTime: 'X-Mappersmith-Retry-Time',
maxRetryTimeInSecs: 5,
initialRetryTimeInSecs: 0.1,
factor: 0.2, // randomization factor
multiplier: 2, // exponential factor
retries: 5, // max retries
validateRetry: (response) => response.responseStatus >= 500 // a function that returns true if the request should be retried
})
Automatically configure your requests with a default timeout
import TimeoutMiddleware from 'mappersmith/middleware/timeout'
const Timeout = TimeoutMiddleware(500)
const client = forge({
middleware: [ Timeout ],
/* ... */
})
client.User.all()
** The default timeout can be overridden with the explicit use of the timeout parameter, example:
client.User.all({ timeout: 100 })
// timeout will be 100 instead of 500
This section is only relevant for mappersmith versions older than but not including 2.27.0
, when the method prepareRequest
did not exist. This section describes how to create a middleware using older versions.
Since version 2.27.0
a new method was introduced: prepareRequest
. This method aims to replace the request
method in future versions of mappersmith, it has a similar signature as the response
method and it is always async. All previous middleware are backward compatible, the default implementation of prepareRequest
will call the request
method if it exists.
The request
method receives an instance of the Request object and it must return a Request. The method enhance
can be used to generate a new request based on the previous one.
Example:
const MyMiddleware = () => ({
request(request) {
return request.enhance({
headers: { 'x-special-request': '->' }
})
},
response(next) {
return next().then((response) => response.enhance({
headers: { 'x-special-response': '<-' }
}))
}
})
The request phase can be asynchronous, just return a promise resolving a request. Example:
const MyMiddleware = () => ({
request(request) {
return Promise.resolve(
request.enhance({
headers: { 'x-special-token': 'abc123' }
})
)
}
})
Mappersmith plays nice with all test frameworks, the generated client is a plain javascript object and all the methods can be mocked without any problem. However, this experience can be greatly improved with the test library.
The test library has 4 utilities: install
, uninstall
, mockClient
and mockRequest
They are used to setup the test library, example using jasmine:
import { install, uninstall } from 'mappersmith/test'
describe('Feature', () => {
beforeEach(() => install())
afterEach(() => uninstall())
})
mockClient
offers a high level abstraction, it works directly on your client mocking the resources and their methods.
It accepts the methods:
resource(resourceName)
, ex: resource('Users')
method(resourceMethodName)
, ex: method('byId')
with(resourceMethodArguments)
, ex: with({ id: 1 })
status(statusNumber | statusHandler)
, ex: status(204)
or status((request, mock) => 200)
response(responseData | responseHandler)
, ex: response({ user: { id: 1 } })
or response((request, mock) => ({ user: { id: request.body().id } }))
assertObject()
assertObjectAsync()
Example using jasmine:
import forge from 'mappersmith'
import { install, uninstall, mockClient } from 'mappersmith/test'
describe('Feature', () => {
beforeEach(() => install())
afterEach(() => uninstall())
it('works', (done) => {
const myManifest = {} // Let's assume I have my manifest here
const client = forge(myManifest)
mockClient(client)
.resource('User')
.method('all')
.response({ allUsers: [{id: 1}] })
// now if I call my resource method, it should return my mock response
client.User
.all()
.then((response) => expect(response.data()).toEqual({ allUsers: [{id: 1}] }))
.then(done)
})
})
To mock a failure just use the correct HTTP status, example:
// ...
mockClient(client)
.resource('User')
.method('byId')
.with({ id: 'ABC' })
.status(422)
.response({ error: 'invalid ID' })
// ...
The method with
accepts the body and headers attributes, example:
// ...
mockClient(client)
.with({
id: 'abc',
headers: { 'x-special': 'value'},
body: { payload: 1 }
})
// ...
It's possible to use a match function to assert params and body, example:
import { m } from 'mappersmith/test'
mockClient(client)
.with({
id: 'abc',
name: m.stringContaining('john'),
headers: { 'x-special': 'value'},
body: m.stringMatching(/token=[^&]+&other=true$/)
})
The assert object can be used to retrieve the requests that went through the created mock, example:
const mock = mockClient(client)
.resource('User')
.method('all')
.response({ allUsers: [{id: 1}] })
.assertObject()
console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())
The mock
object is an instance of MockAssert
and exposes three methods:
Request
array;null
if array is empty.Note:
The assert object will also be returned in the mockRequest
function call.
If you have a middleware with an async request phase use assertObjectAsync
to await for the middleware execution, example:
const mock = await mockClient(client)
.resource('User')
.method('all')
.response({ allUsers: [{id: 1}] })
.assertObjectAsync()
console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())
response
and status
can accept functions to generate response body or status. This can be useful when you want to return different responses for the same request being made several times.
const generateResponse = () => {
return (request, mock) => mock.callsCount() === 0
? {}
: { user: { id: 1 } }
}
const mock = mockClient(client)
.resource('User')
.method('create')
.response(generateResponse())
mockRequest
offers a low level abstraction, very useful for automations.
It accepts the params: method, url, body and response
It returns an assert object
Example using jasmine:
import forge from 'mappersmith'
import { install, uninstall, mockRequest } from 'mappersmith/test'
describe('Feature', () => {
beforeEach(() => install())
afterEach(() => uninstall())
it('works', (done) => {
mockRequest({
method: 'get',
url: 'https://my.api.com/users?someParam=true',
response: {
body: { allUsers: [{id: 1}] }
}
})
const myManifest = {} // Let's assume I have my manifest here
const client = forge(myManifest)
client.User
.all()
.then((response) => expect(response.data()).toEqual({ allUsers: [{id: 1}] }))
.then(done)
})
})
A more complete example:
// ...
mockRequest({
method: 'post',
url: 'http://example.org/blogs',
body: 'param1=A¶m2=B', // request body
response: {
status: 503,
body: { error: true },
headers: { 'x-header': 'nope' }
}
})
// ...
It's possible to use a match function to assert the body and the URL, example:
import { m } from 'mappersmith/test'
mockRequest({
method: 'post',
url: m.stringMatching(/example\.org/),
body: m.anything(),
response: {
body: { allUsers: [{id: 1}] }
}
})
Using the assert object:
const mock = mockRequest({
method: 'get',
url: 'https://my.api.com/users?someParam=true',
response: {
body: { allUsers: [{id: 1}] }
}
})
console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())
mockClient
and mockRequest
accept match functions, the available built-in match functions are:
import { m } from 'mappersmith/test'
m.stringMatching(/something/) // accepts a regexp
m.stringContaining('some-string') // accepts a string
m.anything()
m.uuid4()
A match function is a function which returns a boolean, example:
mockClient(client)
.with({
id: 'abc',
headers: { 'x-special': 'value'},
body: (body) => body === 'something'
})
Note:
mockClient
only accepts match functions for body and params
mockRequest
only accepts match functions for body and url
Mappersmith has a pluggable transport layer and it includes by default three gateways: xhr, http and fetch. Mappersmith will pick the correct gateway based on the environment you are running (nodejs or the browser).
You can write your own gateway, take a look at XHR for an example. To configure, import the configs
object and assign the gateway option, like:
import { configs } from 'mappersmith'
configs.gateway = MyGateway
It's possible to globally configure your gateway through the option gatewayConfigs
.
When running with node.js you can configure the configure
callback to further customize the http/https
module, example:
import fs from 'fs'
import https from 'https'
import { configs } from 'mappersmith'
const key = fs.readFileSync('/path/to/my-key.pem')
const cert = fs.readFileSync('/path/to/my-cert.pem')
configs.gatewayConfigs.HTTP = {
configure() {
return {
agent: new https.Agent({ key, cert })
}
}
}
The new configurations will be merged. configure
also receives the requestParams
as the first argument. Take a look here for more options.
The HTTP gatewayConfigs also provides several callback functions that will be called when various events are emitted on the request
, socket
, and response
EventEmitters. These callbacks can be used as a hook into the event cycle to execute any custom code.
For example, you may want to time how long each stage of the request or response takes.
These callback functions will receive the requestParams
as the first argument.
The following callbacks are supported:
onRequestWillStart
- This callback is not based on a event emitted by Node but is called just before the request
method is called.onRequestSocketAssigned
- Called when the 'socket' event is emitted on the request
onSocketLookup
- Called when the lookup
event is emitted on the socket
onSocketConnect
- Called when the connect
event is emitted on the socket
onSocketSecureConnect
- Called when the secureConnect
event is emitted on the socket
onResponseReadable
- Called when the readable
event is emitted on the response
onResponseEnd
- Called when the end
event is emitted on the response
let startTime
configs.gatewayConfigs.HTTP = {
onRequestWillStart() {
startTime = Date.now()
}
onResponseReadable() {
console.log('Time to first byte', Date.now() - startTime)
}
}
When running in the browser you can configure withCredentials
and configure
to further customize the XMLHttpRequest
object, example:
import { configs } from 'mappersmith'
configs.gatewayConfigs.XHR = {
withCredentials: true,
configure(xhr) {
xhr.ontimeout = () => console.error('timeout!')
}
}
Take a look here for more options.
Mappersmith does not apply any polyfills, it depends on a native fetch
implementation to be supported. It is possible assign the fetch implementation used by Mappersmith:
import { configs } from 'mappersmith'
configs.fetch = fetchFunction
Fetch is not used by default, you can configure it through configs.gateway
.
import FetchGateway from 'mappersmith/gateway/fetch'
import { configs } from 'mappersmith'
configs.gateway = FetchGateway
// Extra configurations, if needed
configs.gatewayConfigs.Fetch = {
credentials: 'same-origin'
}
Take a look here for more options.
Mappersmith also supports TypeScript (>=3.5). In the following sections there are some common examples for using TypeScript with Mappersmith where it is not too obvious how typings are properly applied.
To create a middleware using TypeScript you just have to add the Middleware
interface to your middleware object:
import { Middleware } from 'mappersmith'
const MyMiddleware: Middleware = () => ({
prepareRequest(next) {
return next().then(request => request.enhance({
headers: { 'x-special-request': '->' }
}))
},
response(next) {
return next().then(response => response.enhance({
headers: { 'x-special-response': '<-' }
}))
}
})
mockClient
with TypeScriptTo use the mockClient
with proper types you need to pass a typeof your client as generic to the mockClient
function:
import forge from 'mappersmith'
import { mockClient } from 'mappersmith/test'
const github = forge({
clientId: 'github',
host: 'https://status.github.com',
resources: {
Status: {
current: { path: '/api/status.json' },
messages: { path: '/api/messages.json' },
lastMessage: { path: '/api/last-message.json' },
},
},
})
const mock = mockClient<typeof github>(github)
.resource('Status')
.method('current')
.with({ id: 'abc' })
.response({ allUsers: [] })
.assertObject()
console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())
mockRequest
with Typescriptconst mock = mockRequest({
method: 'get',
url: 'https://status.github.com/api/status.json',
response: {
status: 503,
body: { error: true },
}
})
console.log(mock.mostRecentCall())
console.log(mock.callsCount())
console.log(mock.calls())
yarn test:browser
yarn test:node
yarn integration-server &
yarn test:browser:integration
yarn test:node:integration
yarn test
NODE_ENV=production yarn build
Check it out!
https://github.com/tulios/mappersmith/graphs/contributors
See LICENSE for more details.
2.33.3
GatewayConfiguration
typingsFAQs
It is a lightweight rest client for node.js and the browser
The npm package mappersmith receives a total of 145,904 weekly downloads. As such, mappersmith popularity was classified as popular.
We found that mappersmith demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
At its inaugural meeting, the JSR Working Group outlined plans for an open governance model and a roadmap to enhance JavaScript package management.
Security News
Research
An advanced npm supply chain attack is leveraging Ethereum smart contracts for decentralized, persistent malware control, evading traditional defenses.
Security News
Research
Attackers are impersonating Sindre Sorhus on npm with a fake 'chalk-node' package containing a malicious backdoor to compromise developers' projects.