Since I started working with Trails, I have come to like its approach to a lot of things. I feel its what I have always been looking for in a Node web framework. At the moment the documentation is not completed but there is enough information to got you started with Trails.
From my last post you can see I am trying to build something with the Instagram API, I had to
change from using the bell for social authentication to the actual [Instagram node library][],
the reason for this was that both were not compatible with each other (this however may be due to my
lack of experience, if you know how to get them to work together reach out to me please). When I
changed the code I started by writing the social auth in InstagramController
, auth
method.
For authentication I was following an article about building a an instagram postcard app and
they suggested using Bluebird to convert callbacks into promises. This is what my InstagramController
looked like after I was finished:
'use strict'
const instagramApi = require('instagram-node').instagram()
const Bluebird = require('bluebird')
const Controller = require('trails/controller')
/**
* Promisify the instagram API library
*/
Bluebird.promisifyAll(instagramApi)
/**
* @module InstagramAuthController
* @description Social authentication with Instagram.
*/
module.exports = class InstagramController extends Controller {
auth(request, reply) {
const instagramRedirectUri = this.app.config.get('instagram_redirect_uri')
const clientId = this.app.config.get('instagram_client_id')
const clientSecret = this.app.config.get('instagram_client_secret')
instagramApi.use({
client_id: clientId,
client_secret: clientSecret
})
reply.redirect(instagramApi.get_authorization_url(redirectUri))
}
handleAuth(request, reply) {
const instagramRedirectUri = this.app.config.get('instagram_redirect_uri')
instagramApi.authorize_userAsync(request.query.code, redirectUri)
.then(function (result) {
request.cookieAuth.set({
accessToken: result.access_token
})
return reply.redirect('/')
})
.catch(function (errors) {
console.log('Blowing up: ', errors)
})
}
}
This was looking good, It was doing the social authentication just as I wanted it to, I could now
move on to getting actual data from Instagram and start building the rest of my app. I decided
to just test this out by using the default ViewController
that is created when you first run
the Trails generator. I wrote some code in it that look like the code below:
'use strict'
const instagramApi = require('instagram-node').instagram()
const Bluebird = require('bluebird')
const Controller = require('trails/controller')
/**
* Promisify the instagram API library
*/
Bluebird.promisifyAll(instagramApi)
module.exports = class ViewController extends Controller {
helloWorld (request, reply) {
const { accessToken } = request.auth.credentials
if (accessToken) {
instagramApi.use({ access_token: accessToken })
instagramApi.user_self_media_recentAsync(10)
.then(function (medias, pagination, remaining, limit) {
reply(medias.map(function (image) {
return image.images.standard_resolution.url
}))
})
.catch(function (errors) {
console.log(errors)
})
} else {
reply('Hello Trails.js')
}
}
}
I won’t be going through what each part of the code is doing, but have you noticed something common amongst the two controllers? They both seem to be repeating code.
Let try and keep it DRY
So you can see in both controllers we are requiring the Instagram api library and the
Bluebird library. But not just that alone, we are also Promisifying (not even sure
this is a word) the Instagram in both controllers, we can reduce this by moving our code into
what Trails call a Service (this is not specific to Trails as you will find service class
in other programming languages and frameworks). Before we start doing this, we can also start to
think of how we want our dependencies to work, wether we should pass down an entire object or just
pass down what we need. I am from a [PHP][] background where we advocate dependency injection heavily.
I made the decision that my Service should be as agnostic to the framework I am using as much
as it can. This is what my InstagramService
looks like:
'use strict'
const instagramApi = require('instagram-node').instagram()
const Bluebird = require('bluebird')
const Service = require('trails/service')
/**
* Promisify the instagram API library
*/
Bluebird.promisifyAll(instagramApi)
/**
* @module InstagramService
* @description Deals with all interaction with instagram
*/
module.exports = class InstagramService extends Service {
authenticate(clientId, clientSecret, redirectUri) {
instagramApi.use({
client_id: clientId,
client_secret: clientSecret
})
return instagramApi.get_authorization_url(redirectUri)
}
authorize(code, redirectUri) {
return instagramApi.authorize_userAsync(code, redirectUri);
}
getRecent(accesToken, limit=10) {
return this._getApi(accesToken).user_self_media_recentAsync({count: limit})
}
_getApi(accessToken) {
instagramApi.use({ access_token: accessToken })
return instagramApi
}
}
Now with my new InstagramService
class in place, I can start using it in places where I
was repeating code before, so now I can revisit my InstagramController
and my ViewController
and make the changes necessary. We can remove the code from the InstagramController
auth
method
to:
auth(request, reply) {
const instagramRedirectUri = this.app.config.get('instagram_redirect_uri')
const clientId = this.app.config.get('instagram_client_id')
const clientSecret = this.app.config.get('instagram_client_secret')
const instagramAuthUri = this.app.services.InstagramService.authenticate(
clientId,
clientSecret,
instagramRedirectUri
)
reply.redirect(instagramAuthUri)
}
I know what you are probably thinking, that didn’t change much of the code. It doesn’t even look
like it saved that many lines of code either, but we did move logic that’s relating to Instagram
api to somewhere else, this also made sure that in the InstagramController
we no longer need:
const instagramApi = require('instagram-node').instagram()
const Bluebird = require('bluebird')
/**
* Promisify the instagram API library
*/
Bluebird.promisifyAll(instagramApi)
This is also true for the ViewController
, but lets move on to the handleAuth
method:
handleAuth(request, reply) {
const instagramRedirectUri = this.app.config.get('instagram_redirect_uri')
this.app.services.InstagramService.authorize(
request.query.code,
instagramRedirectUri
)
.then(function (result) {
request.cookieAuth.set({
accessToken: result.access_token
})
return reply.redirect('/')
})
.catch(function (errors) {
console.log('Blowing up: ', errors)
})
}
Oh man, this still seem like we haven’t done that much changes. But we have hidden a bit of implementation detail.
Hiding implementation details isn’t a bad thing, it allows us to have a simple API to interact with our code whose details are hidden
Now let us move on to the ViewController
to see how this has helped. I can now refer to the same
service again which will have my promisified version of the Instagram api ready to use.
helloWorld (request, reply) {
const { accessToken } = request.auth.credentials
if (accessToken) {
this.app.services.InstagramService.getRecent(accessToken, 5)
.then(function (medias, pagination, remaining, limit) {
reply(medias.map(function (image) {
return image.images.standard_resolution.url
}))
})
.catch(function (errors) {
console.log(errors)
})
} else {
reply('Hello Trails.js')
}
}
Do note I don’t have any databse setup in place as yet, currently I am just storing the Instagram access token in session cookie.