api-mount-server

Library for making communication between front-end and back-end simple

Usage no npm install needed!

<script type="module">
  import apiMountServer from 'https://cdn.skypack.dev/api-mount-server';
</script>

README

api-mount-server

by Vytenis Urbonavičius

api-mount-server library provides a straightforward way of exposing API from a Node.js server application for consumption on a client side using api-mount-client library.


Installation

Consider using a more universal library - api-mount. api-mount contains both server and client code in one package. Usage and available methods are identical to api-mount-server. Only package name differs.

If you want to continue using a server-only version:

npm install --save api-mount-server

Usage

Constructing API objects


This library allows exposing API objects. These objects can be constructed in multiple ways.

Simple object with methods:

const api = {
  foo() {
    return 'foo'
  },
}

Set of functions collected into an API object:

// api.js
export const foo = () => 'foo'
// mount.js
import * as api from './api'
// ...

Object created from a class

class SomeClass {
  foo() {
    return 'foo'
  }
}

const api = new SomeClass()

Static class (if supported)

class StaticClass {
  static foo() {
    return 'foo'
  }
}

There may be more ways but these are just a few examples to showcase the possibilities.

Writing API methods


API methods are normal methods or functions with one constraint - they can only operate with serializable values (arguments, return value). In this case serializable value is a value which can be converted into JSON using JSON.stringify() method.

In addition to the rule above, API may be asynchronous - it may return a Promise if needed. It can also throw an exception.

Asynchronous API function example:

// Using timer to simulate asynchronous behavior
const foo = () => new Promise(resolve => setTimeout(() => resolve('foo'), 1000))

Synchronous API function example:

const foo = () => 'foo'

Please note that even when synchronous API is used, client will see these return values as promises which can be extracted either with .then or with async keyword:

// ...

console.log(await API.foo()) // 'foo'

// or

API.foo().then(console.log) // 'foo'

// ...

You can find more information about how API can be accessed from client side here: api-mount-client

Exposing API object


import {apiMountFactory} from 'api-mount-server'

const api = {
  /* ... API methods ... */
}

const ApiMount = apiMountFactory()
ApiMount.exposeApi(api)

When exposing non-class-based objects, one needs to be aware that API will not be namespaced by default. In other words, foo will be accessible via HTTP directly as:

  /foo

This may cause trouble when exposing multiple APIs - name clashes can cause trouble, also api methods from several API objects may be fused together into a single API object from the perspective of a client.

One possible solution is to provide namespaces manually like this:

ApiMount.exposeApi(api, {basePath: '/some-namespace'})

There is also an easier way for those who prefer using classes. Namespaces are added automatically when using exposeClassBasedApi:

class SomeClass {
  foo() {
    return 'foo'
  }
}

ApiMount.exposeClassBasedApi(new SomeClass())

In this case foo will become available via HTTP as:

/some-class/foo

If one uses configuration which supports static classes (i.e. TypeScript), following approach can be used:

class StaticClass {
  static foo() {
    return 'foo'
  }
}

ApiMount.exposeClassBasedApi(StaticClass)

In this case foo will become accessible via HTTP as:

/static-class/foo

CORS


One common problem when starting to use api-mount-server is that requests are blocked by browser's CORS policy. Depending on client configuration symptoms may be either completely blocked requests or opaque responses (i.e. missing response information).

There are multiple ways to solve this problem like serving from same domain as client, using proxies, etc. However, one of the quickest solutions/workarounds is using cors npm package when serving API to state that cross-domain requests should be accepted:

// ...
const ApiMount = apiMountFactory({
  beforeListen: app => {
    // This is just for testing purposes
    // You would probably want to explicitly list
    // where you expect requests to be coming from
    // for security reasons
    app.use(require('cors')())
  },
})
// ...

Supported Configuration

As one could see in the above examples, it is possible to pass configuration object which alters behavior of api-mount. All these methods accept configuration object as (last) argument:

  • apiMountFactory
  • exposeClassBasedApi
  • exposeApi

Configuration object may contain following keys:

  • name - express app name - only needed in corner case when there are several express apps initialized manually at the same time (using injectLaunchCode).
  • basePath - path to be added to HTTP address before automatically generated part. Mostly useful for name-spacing.
  • beforeListen - hook for altering Express configuration. It is very useful for things like CORS configuration, etc. This hook will not fire nor it is needed if custom launch code is injected.
  • beforeExecution - hook for injecting logic before handling api request.
  • beforeResponse - hook for customizing server response logic.
  • afterResponse - hook for injecting logic after server responds.
  • port - server port number - only available when calling apiMountFactory.

Should one want to customize how express app is initialized, this is how it can be done:

injectLaunchCode(() => {
  const newApp = express()
  newApp.use(bodyParser.urlencoded({extended: false}))
  newApp.use(bodyParser.json())
  newApp.listen(3007)
  return newApp
})

Above example would override default express app initialization. However, should one want to have several different ways to initialize it, injectLaunchCode supports a second argument - name. It must match name which is provided in configuration object when exposing api or using apiMountFactory.

More information can be found in docs directory inside the api-mount-server package. Also, code suggestions should be available provided a compatible IDE is used (such as VSCode).

Protocol

api-mount-server is designed to expose API from a Node.js server. However, one might want to expose API from a different kind of back-end and still be able to consume it via api-mount-client. In order to do that one has to follow rules explained below. Note that although rules are customizable, below explanation describes default behavior.

Each API method name should be changed to param-case and exposed as HTTP path.

In case of successful response, HTTP status should be 200.

In case of error response (promise rejection or exception), HTTP status should be 500.

All requests should be of method POST.

All arguments for API methods should be provided via body JSON which looks like this:

{
  "args": []
}

Argument values should be listed under args in a JSON format.

Response should be returned as a JSON object which either carries method return value or error information. As long as response is a valid JSON, there are no other defined constraints.

TypeScript

When consuming some API exposed by api-mount-server, it would be convenient to have code suggestions (typings) available. This functionality is currently out of scope for api-mount but can be achieved with a reasonably minor effort.

If one is developing using TypeScript, it allows generating .d.ts files.

Let's say our API is a class defined inside api.ts file. TSC can generate a corresponding api.d.ts file for it. This file could then be shared with client in some way. For example - one could publish a package containing these generated typings every time server gets published.

When client creates an api object using api-mount-client, this object/class could then be matched with a generated d.ts.