js-data-http

Slack Status npm version npm version Circle CI npm downloads Coverage Status

js-data logo

js-data-http is an HTTP adapter for JSData.

Table of contents

Quick Start

Browser:

npm install --save js-data@rc js-data-http@rc

or

bower install --save js-data@rc js-data-http@rc

or install from a CDN:

Load js-data-http.js after js-data.js.

import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const httpAdapter = new HttpAdapter();
const store = new DataStore();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('school');
store.defineMapper('student');

// GET /school/1
store.find('school', 1).then((school) => {
  console.log('school');
});

Node.js:

npm install --save axios js-data@rc js-data-http-node@rc
import { Container } from 'js-data';
import { HttpAdapter } from 'js-data-http-node';

const httpAdapter = new HttpAdapter({
	basePath: 'https://mydomain.com'
});
const store = new Container();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('school');
store.defineMapper('student');

// GET /school/1
store.find('school', 1).then((school) => {
  console.log('school');
});

Dependencies

  • js-data-http.js bundles axios and depends on js-data
  • js-data-fetch depends on js-data
  • js-data-http-node depends on js-data and optionally axios

See also JSData's dependencies.

Configuring the adapter

The JSData HTTP adapter can be configured upon instantiation and anytime thereafter. See the configuration options

import { HttpAdapter } from 'js-data-http';

const options = {
  basePath: 'https://mydomain.com'
};

// pass options to the constructor
const httpAdapter = new HttpAdapter(options);

console.log(httpAdapter.basePath); // "https://mydomain.com"

httpAdapter.basePath = 'https://otherdomain.com';

console.log(httpAdapter.basePath); // "https://otherdomain.com"

Custom deserialization

When an HTTP request has completed, the HTTP adapter must get the data out of the response so the data can be forwarded to JSData. The HTTP adapter does this with a deserialize method. The default deserialize method takes the response object returned by the underlying HTTP library and extracts the data property and forwards it to JSData, basically like this: return response.data; The adapter's deserialize method is used by the adapter's CRUD methods (find, findAll, update, etc.).

Depending on how your server formats response data, you may find it necessary to customize the HTTP adapter's deserialize method. Here are a bunch of examples:

import { HttpAdapter } from 'js-data-http';

const options = {
  basePath: 'https://mydomain.com',
  deserialize: function (mapper, response, opts) {
    if (/* some condition */) {
      // return something custom
    }
    // Else, do default behavior
    return HttpAdapter.prototype.deserialize.call(this, mapper, id, opts);
  }
};

// pass options to the constructor
const httpAdapter = new HttpAdapter(options);
import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const options = {
  basePath: 'https://mydomain.com'
};

const httpAdapter = new HttpAdapter(options);
const store = new DataStore();
store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('book', {
  deserialize: function (mapper, response, opts) {
    return response.data.books;
  }
});
import { DataStore } from 'js-data';
import { HttpAdapter } from 'js-data-http';

const options = {
  basePath: 'https://mydomain.com'
};

// pass options to the constructor
const httpAdapter = new HttpAdapter(options);
const store = new DataStore();
store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('book');

const query = {};
const options = {
  params: { details: true },
  deserialize: function (BookMapper, response, opts) {
    return response.data.items;
  }
};

// GET https://mydomain.com/book?details=true
store.findAll('book', query, options)
  .then((bookDetails) => {
    console.log(bookDetails);
  })
import { HttpAdapter } from 'js-data-http';

class CustomHttpAdapter extends HttpAdapter {
  deserialize(mapper, response, opts) {
    if (/* some condition */) {
      // return something custom
    }
    // Else, do default behavior
    return super.deserialize(mapper, response, opts);
  }
}
const options = {
  basePath: 'https://mydomain.com'
};

// pass options to the constructor
const httpAdapter = new CustomHttpAdapter(options);

Extending the adapter

The options arguments passed to the HttpAdapter constructor function is mixed directly into the new instance. This allows you to extend the adapter during construction:

import { HttpAdapter } from 'js-data-http';

const options = {
  basePath: 'https://mydomain.com',
  find: function (mapper, id, opts) {
    if (/* some condition */) {
      // do something custom
    }
    // Else, do default behavior
    return HttpAdapter.prototype.find.call(this, mapper, id, opts);
  }
};

// pass options to the constructor
const httpAdapter = new HttpAdapter(options);

You can also extend the HttpAdapter class:

import { HttpAdapter } from 'js-data-http';

class CustomHttpAdapter extends HttpAdapter {
  find(mapper, id, opts) {
    if (/* some condition */) {
      // do something custom
    }
    // Else, do default behavior
    return super.find(mapper, id, opts);
  }
}
const options = {
  basePath: 'https://mydomain.com'
};

// pass options to the constructor
const httpAdapter = new CustomHttpAdapter(options);
var HttpAdapter = require('js-data-http').HttpAdapter;

var CustomHttpAdapter = HttpAdapter.extend({
  find: function (mapper, id, opts) {
    if (/* some condition */) {
      // do something custom
    }
    // Else, do default behavior
    return HttpAdapter.prototype.find.call(this, mapper, id, opts);
  }
});

var options = {
  basePath: 'https://mydomain.com'
};

// pass options to the constructor
var httpAdapter = new CustomHttpAdapter(options);

HTTP Actions

Try the HTTP actions live demo.

import { DataStore } from 'js-data';
import { HttpAdapter, addAction } from 'js-data-http';

const store = new DataStore();
const httpAdapter = new HttpAdapter();

store.registerAdapter('http', httpAdapter, { 'default': true });

store.defineMapper('school', {
  endpoint: 'schools'
});

// Setup action: GET /reports/schools/:school_id/teachers
addAction('getTeacherReports', {
  pathname: 'teachers',
  method: 'GET'
})(store.getMapper('school'));

// GET /reports/schools/1234/teachers
store.getMapper('school').getTeacherReports(1234, {
  basePath: 'reports'
}).then((response) => {
  console.log('response', response.data);
});
import { DataStore } from 'js-data';
import { HttpAdapter, addAction } from 'js-data-http';

const store = new DataStore();
const httpAdapter = new HttpAdapter();

store.registerAdapter('http', httpAdapter, { 'default': true });

// Setup action: GET /reports/schools/:school_id/teachers
@addAction('getTeacherReports', {
  pathname: 'teachers',
  method: 'GET'
});
store.defineMapper('school', {
  endpoint: 'schools'
});

// GET /reports/schools/1234/teachers
store.getMapper('school').getTeacherReports(1234, {
  basePath: 'reports'
}).then((response) => {
  console.log('response', response.data);
});

Using the HTTP adapter's lifecycle hooks

Like JSData itself, the HTTP adapter has lifecycle hooks. You can find them enumerated in the API Reference Documentation. The HTTP adapter has the CRUD lifecycle hooks similar to JSData, but adds some HTTP-specific lifecycle hooks, like beforeGET, beforeHTTP, afterPOST, etc.

Here are some demos:

Here's an example of using the beforeHTTP hook as an HTTP interceptor to set a header:

import { HttpAdapter } from 'js-data-http';

const options = {
  basePath: 'https://mydomain.com',
  beforeHTTP: function (config, opts) {
    config.headers || (config.headers = {});
    config.headers.authorization = `Bearer ${localStorage.get('auth_token')}`;
    
    // Now do the default behavior
    return HttpAdapter.prototype.beforeHTTP.call(this, config, opts);
  }
};

// pass options to the constructor
const httpAdapter = new HttpAdapter(options);
import { HttpAdapter } from 'js-data-http';

class CustomHttpAdapter extends HttpAdapter {
  beforeHTTP(config, opts) {
    config.headers || (config.headers = {});
    config.headers.authorization = `Bearer ${localStorage.get('auth_token')}`;
    
    // Now do the default behavior
  	return super.beforeHTTP(config, opts);
  } 
}

const options = {
  basePath: 'https://mydomain.com'
};

// pass options to the constructor
const httpAdapter = new CustomHttpAdapter(options);

Using window.fetch

You can configure the HTTP adapter to use window.fetch if available by setting the useFetch option to true. window.fetch is experimental, so be sure to polyfill it if necessary.

Here's a polyfill: https://github.com/github/fetch

Using a custom HTTP library

To use a custom HTTP implementation, set the http option when instantiating the adapter. The http option must be a function that takes a single config argument and returns a promise that resolves with the HTTP response. The config argument that will be passed to your function follows the config format required by axios. The expected response format matches that of axios as well.

If you're using the HTTP adapter in the browser and you want to avoid loading axios, then install js-data-fetch instead of js-data-http. js-data-fetch is identical to js-data-http, except it does not bundle axios, perfect for when you want to use a custom HTTP library with the HTTP adapter.

Try the a live demo that uses superagent as the HTTP underlying library.

Here's an example that makes the HTTP adapter use the $http component from Angular 1.x:

app.service('httpAdapter', ($http) => {
  return new JSDataHttp.HttpAdapter({
    http: $http
  });
});

Links

🚧

See an issue with this tutorial?

You can open an issue or better yet, suggest edits right on this page.

📘

Need support?

Have a technical question? Post on the JSData Stack Overflow channel or the Mailing list.

Want to chat with the community? Hop onto the JSData Slack channel.