How to Build a Light REST Client With JavaScript
We take a look at how to build a REST client using the free backend as a service platform, Backendless, and a good amount of JavaScript code.
Join the DZone community and get the full member experience.
Join For FreeSome 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. Today we're going to show you how to build a custom and very light API client library for working with the 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.
Setup Backendless
Make sure you have a Backendless developer account. If you do not have one, you can register for a free account at https://develop.backendless.com.
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
.
Setup a New NPM Module
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 the NPM module.
cd rest-api-client
npm init
Leave all the settings as default and open the project in a code editor that you're comfortable with.
Install Backendless Request
As I mentioned at the beginning, we use the package in the all of our JS services, in a Node.js 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'
Generic API Client
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 persistenced 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. In the beginning, in the constructor, we just store the base API path. It may be any base URL to our API server such as http://our-server-host.com/api
orhttp://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 addMiddleware
method expects two arguments: the first is an optional “method” – REST method (‘get’
, ‘post’
, ‘put’
, ‘delete’
, …) and handler
for decoration requests before sending. If we want that handler to be applied for all of the requests, we just need to pass to the addMiddleware
method one argument, handler
, but in the event that we want that handler to be applied only for 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 every 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.
User Auth Settings
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:
- In the constructor, we should get key of the auth header and we may already have auth value, so add
authHeaderKey
andauthHeaderValue
right after thebasePath
parameter. - Also, add the following code at the end of the constructor:
this.addMiddleware(request => { if (this.authHeaderValue) { request.set(this.authHeaderKey, this.authHeaderValue) } })
- Add a new class method:
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.
Backendless API Client
Now it’s time to create an API client for working with Backendless API. First, let’s create a new directory backendless
inside our directory src
and create there a new JS file with the name constant.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_API_HOST
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 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, I assume everything is straightforward. Let’s go through other two.
- User
login
– when the API call is finished successfully, the server returns auser
object in the response body and there is an additional property,user-token
, containing the value we should add to all of the next requests. As you recall, we have asetAuth
method in the Client class, so that’s exactly what we need. - User
logout
method – after the user logs out, we just resetuser-token
for the client instance.
Let’s do some refactoring and move 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 end points in the our REST Docs to extend your API client for your specific needs.
Test the Backendless API Client
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 andapiKeys 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 intopackage.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.
Multi API Clients
You also have the ability to have more than one beckandlessApiClient; 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)
External API Client
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 andauthHeaderValue .
const Client = require('../client')
const createExternalApiClient = () => {
const client = new Client({
basePath: `http://my-host.com`,
})
return {
someRequest() {
return client.get('/somthing')
},
}
}
module.exports = createExternalApiClient
Conclusion
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.
Published at DZone with permission of Vladimir Upirov, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments