3
\$\begingroup\$

I am learning to be a full stack developer and have been building out an application using Node JS as a backend. Since I have lots of experience using Angular I love RxJS. So naturally I was tempted to use RxJS for some types of tasks. Are there any down sides to using RxJS on the backend, more specifically are there any downs sides I can learn about from the following code implementation?

Im using Sequalize and Postgres. As you can see in the Invite Service I have used the Observable stream to first check if the Invite id is valid and then it checks if the recipient email is found in the database as a current user account or not. From either of the three returned values to the route I can now tell the front end what it must do next.

Other notable things:

  1. I am using typical JWT authentication which in this case is a special user in the db since the request is being made from a web app and there is no actual logged in user.

  2. I decided to pass the invite id and the recipient email in the headers which I know is kind of unusual.

This web app is actually just a mediator to deal with a complicated invite process which is being initialized from a React Native app.

Entry Point:

import inviteGenRouter from './routes/invite.routes';
let inviteRouter = inviteGenRouter(services);
api.use('/invite', inviteRouter);

app.use('/api', [passport.authenticate('jwt-bearer', {
  session: false,
  failWithError: true
}), (err, req, res, next) => {

  if (err) {
    Logger.error('Authentication Handshake: ', err);
    let error = {
      error: err.name,
      message: err.message
    };
    let statusCode = err.status || 500;
    return res.status(statusCode).json(error);
  }
  return next();
}], api);

Invite Routes:

router.route('/').get((req, res) => {
  if (req.headers.data) {
    return services.InviteService.authenticateInvite(req.headers.data)
      .subscribe(
        _res => {
          // Return options for front end.
          // If string the invite code was not found.
          // If null the associated email was not a user account.
          // If user the associated email was found.
        },
        _error => {
          // Some stuff when wrong...
        }
      );
  } else {
    // return some sort of error code.
  }

});

Invite Service:

class InviteService  {

  constructor(models) {
    this.models = models
  }

  findInviteById(id) {
    return Observable.defer(() => {
      return Observable.fromPromise(this.models.Invite.findOne({
        where: { inviteId: id }
      }));
    });
  }

  findUserById(email) {
    return Observable.defer(() => {
      return Observable.fromPromise(this.models.User.findOne({
        where: { email: email }
      }));
    });
  }

  authenticateInvite(token) {
    let payload = jwt.verify(token, appConfig.SESSION_SECRET);

    return this.findInviteById(payload.inviteId)
      .map(_invite => _invite ? _invite : Observable.of('Invalid Invite ID'))
      .mergeMap(_invite => _invite._isScalar ? _invite : this.findUserById(payload.email));
  }
}

module.exports = InviteService;
\$\endgroup\$
3
\$\begingroup\$

Are there any down sides to using RxJS on the backend

The way I see it, you get 2 benefits from RxJS

  1. Reactivity
  2. Streaming operations

Observables are effectively a form of event management. They run, listen to things, and execute code when something happens. They're a good way to respond when something happens in the app. However, when used in routing, your observable only lives during the duration of the request-response cycle. You lose all the benefits of a persistent event system when they're short-lived.

This leaves you with streaming operations which isn't something regular functions, promises, array methods and ternaries can't do. When observables are used this way, they effectively become what I call "fat promises". You're better off fully utilizing native constructs than using advanced constructs suboptimally.

Speaking of Node, async and await are available too, which makes life a bit easier:

// Routes
router.route('/').get(async (req, res) => {
  try {
    if (req.headers.data) {
      const res = await services.InviteService.authenticateInvite(req.headers.data)
      // respond with data
    } else {
      // respond with no header data error 
    }
  } catch (e){
    // Something else went wrong
  }

});

// Service
class InviteService  {

  constructor(models) {
    this.models = models
  }

  async findInviteById(id) {
    return this.models.Invite.findOne({ where: { inviteId: id } })
  }

  async findUserById(email) {
    return this.models.User.findOne({ where: { email: email } })
  }

  async authenticateInvite(token) {
    const payload = jwt.verify(token, appConfig.SESSION_SECRET)
    const invite = await this.findInviteById(payload.inviteId)

    if(!invite) throw new Error('Invalid Invite ID')

    return invite._isScalar ? invite : this.findUserById(payload.email)
  }
}

module.exports = InviteService;

Also, if you end up doing more comparisons after getting authenticateInvite's return value, that means authenticateInvite is not abstracting correctly. I'd expect that after I call authenticateInvite, it should be boolean. It either resolves (auth passed) or rejects (auth failed) with the error object containing the reason for failure. All business logic (and errors) should go to the service class.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.