Some Backendless users choose to use REST APIs in their JavaScript projects. While many can simply use our pre-packaged JS-SDK, that SDK may not always be able to achieve the result the user is seeking. In this article, we’re going to show you how to build a custom and very light API client library for working with Backendless API.
Some time ago, we created a simple NPM module named “backendless-request” for sending CRUD requests to the server. That package is used in all our services such as DevConsole, JSCodeRunner, JS-SDK, etc.. If you would like to see the sources of the package, you can find it on Github.
Log in to Backendless Console. If you do not have a developer account, you can register for a free account at https://develop.backendless.com/registration.
Create a new Backendless application (you can use a pre-existing application if you have one you’d like to build upon). My application will be called RestApiClient.
Let’s create a new directory and call it rest-api-client. Run the following command in your terminal:
mkdir rest-api-client
Go into the directory and initialize our NPM module.
cd rest-api-client npm init
Leave all settings as default and open the project in a code editor that you’re comfortable with.
As I mentioned at the beginning, we use the package in the all of our JS services, in a NodeJS environment and in a WEB/Browser environment as well, so it’s completely adapted for use everywhere. Also, I want to note that it’s a very light library for making REST requests. The non-minimized build for browsers weighs just 28KB, and the minimized build weighs just 11KB. That sounds good enough, doesn’t it?
Just install the package from NPM by running the following command:
npm i backendless-request -S
Once it’s installed, we can use the package in our project simply by requiring it:
//Common const Request = require('backendless-request') //ES 6 import Request from 'backendless-request'
As you know, our main goal is to build a library for making requests to any servers as well as having the ability to have several API clients at the same time. Therefore, we’re going to create a generic API-client class for keeping baseServerPath and userAuth settings as well, which will be persistent in the instance context. Let’s create a new file ./src/client.js and put the following code in there:
const Request = require('backendless-request') class Client { constructor({ basePath }) { this.basePath = basePath || '' this.middlewares = [] Request.methods.forEach(method => { this[method] = (path, body) => this.request(this.basePath + path, method, body) }) } request(path, method, body) { let request = new Request(path, method, body) this.middlewares.forEach(middleware => { if (!middleware.method || middleware.method === method) { request = middleware.handler(request) || request } }) return request } addMiddleware(method, handler) { if (typeof method === 'function') { handler = method method = undefined } this.middlewares.push({ method, handler }) } } module.exports = Client
Let’s take a more in-depth look at the class to see what exactly the class contains. At the beginning, in the constructor, we just store the base API path. It can be any base url to our API server such as http://our-server-host.com/api or http://localhost:3000. As you can see, the class is simple; in fact, it’s just an additional layer between the client code and a real API request.
In the end, our Client class should look like this:
const Request = require('backendless-request') class Client { constructor({ basePath, authHeaderKey, authHeaderValue }) { this.basePath = basePath || '' this.authHeaderKey = authHeaderKey this.authHeaderValue = authHeaderValue this.middlewares = [] Request.methods.forEach(method => { this[method] = (path, body) => this.request(this.basePath + path, method, body) }) this.addMiddleware(request => { if (this.authHeaderValue) { request.set(this.authHeaderKey, this.authHeaderValue) } }) } setAuth(authHeaderValue) { this.authHeaderValue = authHeaderValue } request(path, method, body) { let request = new Request(path, method, body) this.middlewares.forEach(middleware => { if (!middleware.method || middleware.method === method) { request = middleware.handler(request) || request } }) return request } addMiddleware(method, handler) { if (typeof method === 'function') { handler = method method = undefined } this.middlewares.push({ method, handler }) } } module.exports = Client
Let’s take a look at another important item: middleware. Through it, we can decorate any requests before sending them to the server. We will need this a little bit later for setting the user auth header.
The method addMiddleware expects two arguments: the first is an optional “method” (REST method (‘get’, ‘post’, ‘put’, ‘delete’, …)) and the second is a handler for decorating requests before sending. If we want that handler to be applied for all of the requests, we just need to pass one argument to the addMiddleware method, handler. In the event that we want that handler to be applied only for a specific method – for example, for GET – we should pass both arguments. Below is an example of how to use the addMiddleware method.
const client = new Client(...) client.addMiddleware(handlerForAllRequests) client.addMiddleware('post', handlerForPOSTRequests) client.addMiddleware('get', handlerForGETRequests)
In the code below, we simply create a few API methods and inside each one, we just call the request method with upgraded path parameters. After that, our instance will have all of the methods that BackandlessRequest has.
Request.methods.forEach(method => { this[method] = (path, body) => this.request(this.basePath + path, method, body) })
The request method, in turn, will create a new Request, decorate it with middleware, and process it.
Most likely, you will need to have the ability to add a user auth header to each request for making the request as authorized. So let’s add a few lines in our Client class:
this.addMiddleware(request => { if (this.authHeaderValue) { request.set(this.authHeaderKey, this.authHeaderValue) } })
setAuth(authHeaderValue) { this.authHeaderValue = authHeaderValue }
Now we can easily change the auth state from not-authorized to authorized by calling the setAuth method and passing a token there that we got from a login API request.
Now it’s time to create an API client for working with Backendless API. First, let’s create a new directory called backendless inside our directory src and create there a new JS file with the name constants.js and the following content:
exports.BACKENDLESS_API_HOST = 'https://api.backendless.com' exports.BACKENDLESS_AUTH_KEY = 'user-token'
BACKENDLESS_API_HOST is a host of Backendless API.
BACKENDLESS_AUTH_KEY is a header key which should be sent with a request if we want to make an authorized request.
Now let’s create the ./src/backendless/index.js file with the following content:
const Client = require('../client') const Constants = require('./constants') const usersApi = require('./users') const createBackendlessApiClient = (appId, apiKey) => { const client = new Client({ basePath : `${Constants.BACKENDLESS_API_HOST}/${appId}/${apiKey}`, authHeaderKey: Constants.BACKENDLESS_AUTH_KEY, }) return { users : usersApi(client), } } module.exports = createBackendlessApiClient
As you can see, we don’t have a ./users.js file yet. Let’s fix that. Create a file with that name and the following code:
const Constants = require('./constants') const usersApi = client => { return { register(user) { return client.post(‘/users/register’, user) }, login(login, password) { return client .post(‘/users/login, { login, password }) .then(user => { client.setAuth(user[Constants.BACKENDLESS_AUTH_KEY]) return user }) }, logout() { return client .get(‘/users/logout) .then(() => client.setAuth(null)) } } } module.exports = usersApi
There are just three simple methods for user: register, login, and logout. With the register method, everything should be straightforward. Let’s go through the other two.
Let’s do some refactoring and move the urls into a separate file called ./src/backendless/urls.js. This gives us more flexibility.
const Urls = { users : () => `/users`, userRegister: () => `${Urls.users()}/register`, userLogin : () => `${Urls.users()}/login`, userLogout : () => `${Urls.users()}/logout`, } module.exports = Urls
Next, we’ll modify the ./src/backendless/users.js file a little. When we’re done, the file should look like this:
const Constants = require('./constants') const Urls = require('./urls') const usersApi = client => { return { register(user) { return client.post(Urls.userRegister(), user) }, login(login, password) { return client .post(Urls.userLogin(), { login, password }) .then(user => { client.setAuth(user[Constants.BACKENDLESS_AUTH_KEY]) return user }) }, logout() { return client .get(Urls.userLogout()) .then(() => client.setAuth(null)) } } } module.exports = usersApi
Now, let’s create a simple service for working with a data table. For this service, we should create a new JS file called ./persons.js and modify the ./urls.js and ./index.js files.
./src/beckandless/urls.js:
const Urls = { users : () => `/users`, userObject : objectId => `${Urls.users()}/${objectId}`, userRegister: () => `${Urls.users()}/register`, userLogin : () => `${Urls.users()}/login`, userLogout : () => `${Urls.users()}/logout`, data : () => `/data`, dataTable : tableName => `${Urls.data()}/${tableName}`, dataTableObject: (tableName, objectId) => `${Urls.dataTable(tableName)}/${objectId}`, dataTableCount : tableName => `${Urls.dataTable(tableName)}/count`, } module.exports = Urls
./src/beckandless/persons.js:
const Urls = require('./urls') const TABLE_NAME = 'Person' const personsApi = client => { return { create(person) { return client.post(Urls.dataTable(TABLE_NAME), person) }, find(query) { return client.get(Urls.dataTable(TABLE_NAME)).query(query) }, findById(personId, query) { return client.get(Urls.dataTableObject(TABLE_NAME, personId)).query(query) }, getCount(query) { return client.get(Urls.dataTableCount(TABLE_NAME)).query(query) }, update(person) { return client.put(Urls.dataTableObject(TABLE_NAME, person.objectId), person) }, remove(personId) { return client.delete(Urls.dataTableObject(TABLE_NAME, personId)) }, } } module.exports = personsApi
./src/beckandless/index.js
const Client = require('../client') const Constants = require('./constants') const usersApi = require('./users') const personsApi = require('./persons') const createBackendlessApiClient = (appId, apiKey) => { const client = new Client({ basePath : `${Constants.BACKENDLESS_API_HOST}/${appId}/${apiKey}`, authHeaderKey: Constants.BACKENDLESS_AUTH_KEY, } return { users : usersApi(client), persons: personsApi(client), } } module.exports = createBackendlessApiClient
And finally, create a new index.js file under the src directory:
exports.createBackendlessApiClient = require('./backendless')
You can find more REST endpoints in our REST Docs to extend your API client for your specific needs.
Now, let’s see how it will work. For that, we will write a few simple tests. Create a new JS file called ./tests/index.js with the following content:
const testBackendlessApi = require('./backendless') Promise.resolve() .then(testBackendlessApi) .then(() => console.log('Tests are completed successful!')) .catch(error => { console.error('Tests failed!') console.error(error) process.exit(0) })
Then create another file called ./tests/backendless.js; don’t forget to replace my appId and apiKeys with yours.
const { createBackendlessApiClient } = require('../src') const APP_ID = '43CB29E3-B185-7DC3-FF72-DBBA42DD00' const API_KEY = 'DD9198A8-2FFF-775C-FFFC-9AC434D000' const BackendlessClient = createBackendlessApiClient(APP_ID, API_KEY) module.exports = async function testApi() { const testMarker = `test_${Date.now()}` const userLogin = `foo_${Date.now()}@bar.com` const userPassword = '123456' //create a new unique user const user = await BackendlessClient.users.register({ email : userLogin, password: userPassword, testMarker }) const userId = user.objectId //create a few persons as non-authorized users; property for the objects will be NULL await BackendlessClient.persons.create({ name: `person_${testMarker}`, testMarker }) await BackendlessClient.persons.create({ name: `person_${testMarker}`, testMarker }) await BackendlessClient.persons.create({ name: `person_${testMarker}`, testMarker }) //find only persons what we created before in this test run const persons = await BackendlessClient.persons.find({ where: `testMarker = '${testMarker}'` }) if (persons.length !== 3) { throw new Error(`must be found 3 persons with [testMarker = '${testMarker}']`) } //get count of persons what we created before in this test run const personsCount = await BackendlessClient.persons.getCount({ where: `testMarker = '${testMarker}'` }) if (personsCount !== 3) { throw new Error('number of persons must be equal 3') } //check if every person what we created in this test has no "ownerId" property persons.forEach(person => { if (person.ownerId !== null) { throw new Error('person.ownerId must be NULL, because we\'ve created them as not authorized user') } }) //after user login, all the following requests will be authorized, //and each object what we create will have "ownerId" property, which is equal to objectId of logged user await BackendlessClient.users.login(userLogin, userPassword) //create a few persons as authorized users; property for the objects will be equal to objectId of logged user await BackendlessClient.persons.create({ name: `person_${testMarker}`, testMarker }) await BackendlessClient.persons.create({ name: `person_${testMarker}`, testMarker }) await BackendlessClient.persons.create({ name: `person_${testMarker}`, testMarker }) //find only persons what logged user created before const myPersons = await BackendlessClient.persons.find({ where: `ownerId = '${userId}'` }) if (myPersons.length !== 3) { throw new Error(`must be found 3 persons with [ownerId = '${userId}']`) } //check if every person that user created has correct "ownerId" property myPersons.forEach(person => { if (person.ownerId !== userId) { throw new Error('person.ownerId must be equal logged user id, because we\'ve created them as authorized user') } })
Now we can run the file with the command node ./tests, but I’m going to move that into package.json in the scripts section:
"scripts": { "test": "node ./tests" },
And now to run the tests, just input and run the command npm run test. Do the test a few times and go to your Backendless Dev Console and you will see that some records have the ownerId property and some don’t.
You also have the ability to have more than one backendlessApiClient; just create as many clients as you need and they will work independently of each other.
const { createBackendlessApiClient } = require('../src') const FIRST_APP_ID = 'xxxxxx' const FIRST_API_KEY = 'xxxxxx' const SECOND_APP_ID = 'yyyyyy' const SECOND_API_KEY = 'yyyyyy' const FirstBackendlessClient = createBackendlessApiClient(FIRST_APP_ID, FIRST_API_KEY) const SecondBackendlessClient = createBackendlessApiClient(SECOND_APP_ID, SECOND_API_KEY)
Creating a new API client is almost the same as creating the BackendlessApiClient. Just create a new instance of the Client class and pass the options that you need: basePath, authHeaderKey, and authHeaderValue.
const Client = require('../client') const createExternalApiClient = () => { const client = new Client({ basePath: `http://my-host.com`, }) return { someRequest() { return client.get('/somthing') }, } } module.exports = createExternalApiClient
Today we learned how to create a simple REST API Client using the BackendlessRequest package. We also now know how to easily create a client for working with multiple Backendless apps and how to create a client for external API. The library we created can be used everywhere simply by building it with Webpack and using it in your client application. You can take a look at the source project on Github.
Thanks for reading, I hope the article was helpful for you and see you soon.