DEV Community

Ciro Ivan
Ciro Ivan

Posted on

Idiomatic JavaScript Backend. Part 2

Hi everyone! This part of series Idiomatic JavaScript Backend.

Part 1/3
Part 3/3

Important Information

For best experience please clone this repo: https://github.com/k1r0s/ritley-tutorial. It contains git tags that you can use to travel through different commits to properly follow this tutorial :)

$ git tag

1.preparing-the-env
2.connecting-a-persistance-layer
3.improving-project-structure
4.creating-entity-models
5.handling-errors
6.creating-and-managing-sessions
7.separation-of-concerns
8.everybody-concern-scalability
Enter fullscreen mode Exit fullscreen mode

Go to specific tag

$ git checkout 1.preparing-the-env
Enter fullscreen mode Exit fullscreen mode

Go to latest commit

$ git checkout master
Enter fullscreen mode Exit fullscreen mode

See differences between tags on folder src

$ git diff 1.preparing-the-env 2.connecting-a-persistance-layer src
Enter fullscreen mode Exit fullscreen mode

4. Creating entity models

Ritley doesn't tell you exactly how to build entity models, that's not our concern so I'll try to be brief.

Models encapsulate all logic related with the domain. For instance creating users, encrypting passwords, validate fields, etc. While Resources translate this logic into the HTTP layer.

Our first model will be located into src/models/user.model.js:

import DataService from "../services/database.service";
import EncryptService from "../services/encrypt.service";
import { Provider, Dependency } from "@ritley/decorators";


@Provider.factory
@Dependency("database", DataService)
@Dependency("encrypt", EncryptService)
export default class UserModel {

  static userPublicPredicate = collection => collection.map(({ pass, ...user }) => ({
    ...user
  }))

  validate(payload) {
    const requiredProps = ["name", "pass", "mail"];
    const props = Object.keys(payload);
    if(requiredProps.every(prop => props.includes(prop))) {
      return Promise.resolve();
    } else {
      return Promise.reject();
    }
  }

  create(payload) {
    const pass = this.encrypt.encode(payload.pass);
    return this.database.create("users", { ...payload, pass });
  }

  isUnique({ mail }) {
    return new Promise((resolve, reject) =>
      this.database.exists("users", { mail }).then(reject, resolve));
  }

  searchBy(predicate) {
    return this.readUsers(predicate).then(UserModel.userPublicPredicate);
  }

  readUsers(predicate) {
    if(predicate) {
      return this.database.filter("users", predicate);
    } else {
      return this.database.read("users");
    }
  }

  update(uid, { mail, name }) {
    return this.database.update("users", { uid }, { mail, name });
  }
}
Enter fullscreen mode Exit fullscreen mode

We've just implemented many methods that will be used later on, for now we're going to use validate, isUnique and create to fit requirements on user creation.

Note we've included a new package for password encryption.

Again all non topic packages as such are just placeholders, you can use any other you like :)

we run: $ npm install cpass

Now lets take a look over src/resources/user.resource.js:

 import { AbstractResource } from "@ritley/core";
-import DataService from "../services/database.service";
+import UserModel from "../models/user.model";

-import { Dependency, ReqTransformBodySync } from "@ritley/decorators";
+import { Dependency, ReqTransformBodyAsync } from "@ritley/decorators";

-@Dependency("database", DataService)
+@Dependency("userModel", UserModel)
 export default class UserResource extends AbstractResource {
   constructor() {
     super("/users");
   }

-  @ReqTransformBodySync
-  post(req, res) {
-    const payload = req.body.toJSON();
-    this.database.create("users", payload).then(user => {
-      res.statusCode = 200;
-      res.end(JSON.stringify(user));
-    });
+  @ReqTransformBodyAsync
+  async post(req, res) {
+    const body = await req.body;
+    const payload = body.toJSON();
+    await this.userModel.validate(payload);
+    await this.userModel.isUnique(payload);
+    const user = await this.userModel.create(payload);
+    res.statusCode = 200;
+    res.end(JSON.stringify(user));
   }
 }
Enter fullscreen mode Exit fullscreen mode

As I said before, using async/await feature transforms our post method into a promise, so we're going to use @ReqTransformBodyAsync instead of previous one @ReqTransformBodySync. First one is promise based, so it makes sense to use it with async/await code such as previous snippet.

Of course we've removed this.database calls and DataService from resources. You don't want to mess with persistence layer on your http layer ;)

Our service now fulfills the requirements for user creation but we're missing exception handling here. If JSON isn't well formed, payload doesn't contain required fields, provided email is taken or something we'll held an unhanded rejection or maybe an exception will terminate our app 😰

let's see what's next!


5. Handling exceptions

So, how to file proper responses anytime when an errors pops by?

Well, first of all we need to look at there:

const body = await req.body;
const payload = body.toJSON();
await this.userModel.validate(payload);
await this.userModel.isUnique(payload);
const user = await this.userModel.create(payload);
res.statusCode = 200;
res.end(JSON.stringify(user));
Enter fullscreen mode Exit fullscreen mode

All errors originate from there or subsequent calls and should be handled here (around here) because it involves sending back feedback to the client.

But that is quite difficult and involves a lot of intrusion you may think.

To better understand what means to deal with nested promise rejections in nodejs I recommend this article about promise rejections, or at least keep on the desktop.

Wrapping every specific case with try ... catch can be a nightmare. Lets start by separate every task into new methods that will handle single operations, for instance payload parsing:

parseBody(req, res) {
  try {
    return req.body.toJSON();
  } catch (e) {
    res.statusCode = 400; // Bad Request
    res.end("payload isn't well formed");
  }
}
Enter fullscreen mode Exit fullscreen mode

And of course this works! Lets see how it looks:

import { AbstractResource } from "@ritley/core";
import UserModel from "../models/user.model";

import { Dependency, ReqTransformBodyAsync } from "@ritley/decorators";

@Dependency("userModel", UserModel)
export default class UserResource extends AbstractResource {
  constructor(_database) {
    super("/users");
  }

  @ReqTransformBodyAsync
  async post(req, res) {
    const body = await req.body;
    const payload = this.parseBody(body, res);
    await this.validate(payload, res);
    await this.isUnique(payload, res);
    const user = await this.create(payload, res);
    res.statusCode = 200;
    res.end(JSON.stringify(user));
  }

  parseBody(body, res) {
    try {
      return body.toJSON();
    } catch (e) {
      res.statusCode = 400;
      res.end("payload isn't well formed");
    }
  }

  validate(payload, res) {
    return this.userModel.validate(payload).catch(() => {
      res.statusCode = 400;
      res.end("missing fields, required: [name, mail, pass]");
    })
  }

  isUnique(payload, res) {
    return this.userModel.isUnique(payload).catch(() => {
      res.statusCode = 409;
      res.end("mail is already taken, try another one");
    })
  }

  create(payload, res) {
    return this.userModel.create(payload).catch(() => {
      res.statusCode = 500;
      res.end("there was an error creating your user, try again");
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

mmh! that's huge, does it make sense to expand our code that much only to properly catch exceptions? well...

Even though we're handling errors on every single task that may involve rejections or exceptions we're going to run into UnhandledPromiseRejectionWarning because async generators wraps the whole method into a promise, but we cannot handle post it self, because it gets called by the library and it should not do this by ourselves.

To avoid this we can create a new async method that get's called by post so we can handle async call from outside, kind of a workaround:

post(req, res) {
  this.handledPost(req, res).catch(() => console.log('rejection from inside'));
}

async handledPost() {
  ...lots of awaits that may be rejected but locally handled
}
Enter fullscreen mode Exit fullscreen mode

Another may elegant solution is to use more abstractions since we're repeating the same pattern many times.@ritley/decorators provides some in order to make our life easier, for example:

 import {
+  Default,
+  Catch,
   InternalServerError,
   BadRequest,
   Conflict,
   Created
 } from "@ritley/decorators";
Enter fullscreen mode Exit fullscreen mode

And probably there's not too much to explain:

import { AbstractResource } from "@ritley/core";
import DataService from "../services/database.service";
import UserModel from "../models/user.model";

import {
  Dependency,
  ReqTransformBodyAsync,
  Default,
  Catch,
  InternalServerError,
  BadRequest,
  Conflict,
  Created
} from "@ritley/decorators";

@Dependency("userModel", UserModel)
export default class UserResource extends AbstractResource {
  constructor(_database) {
    super("/users");
  }

  @Default(Created)
  @ReqTransformBodyAsync
  async post(req, res) {
    const payload = await this.parseBody(req, res);
    await this.validate(payload, res);
    await this.isUnique(payload, res);
    return await this.create(payload, res);
  }

  @Catch(BadRequest, "payload isn't well formed")
  parseBody(req) {
    return req.body.then(body => body.toJSON());
  }

  @Catch(BadRequest, "missing fields, required: [name, mail, pass]")
  validate(payload) {
    return this.userModel.validate(payload);
  }

  @Catch(Conflict, "mail is already taken, try another one")
  isUnique(payload) {
    return this.userModel.isUnique(payload);
  }

  @Catch(InternalServerError, "there was an error creating your user, try again")
  create(payload) {
    return this.userModel.create(payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see these abstractions reduce a bit our code base and improve readability.

As you may wonder @Catch(responseFn, content) looks for synchronous exceptions on the method but checks as well if returned value was a promise, if so, adds a catch() callback to it. Either a synchronous error or promise rejection will be handled and responseFn will be invoked with our res <Response> object.

So: BadRequest, Conflict, InternalServerError, Created... are just functions exported by @ritley/decorators that receive a res <Response> object and resolve the proper message to the client. So by calling BadRequest(res, "wrong!") client will receive a HTTP 400 with "wrong!" as a response body.

In the other hand @Default(responseFn) do quite the same but checking for promise resolution using then(). It also attaches a catch() to prevent possible unhandled rejections, but it will be resolved with HTTP 500 such a case, because that error was not properly handled indeed.

In other words Default tells whats going to happen if everything goes well, and Catch wraps sensible calls with an error message, like checkpoints.

But there is even more:

 import { AbstractResource } from "@ritley/core";
-import UserModel from "../models/user.model";
+import UserModel, { UserValidationError, UserMailInUseError } from "../models/user.model";

-import { Dependency, ReqTransformBodyAsync } from "@ritley/decorators";
+import {
+  Dependency,
+  ReqTransformBodyAsync,
+  Default,
+  Throws,
+  InternalServerError,
+  BadRequest,
+  Conflict,
+  Created
+} from "@ritley/decorators";

 @Dependency("userModel", UserModel)
 export default class UserResource extends AbstractResource {
@@ -9,14 +18,16 @@ export default class UserResource extends AbstractResource {
     super("/users");
   }

+  @Throws(SyntaxError, BadRequest)
+  @Throws(UserValidationError, BadRequest)
+  @Throws(UserMailInUseError, Conflict)
+  @Default(Created)
   @ReqTransformBodyAsync
   async post(req, res) {
     const body = await req.body;
     const payload = body.toJSON();
     await this.userModel.validate(payload);
     await this.userModel.isUnique(payload);
-    const user = await this.userModel.create(payload);
-    res.statusCode = 200;
-    res.end(JSON.stringify(user));
+    return this.userModel.create(payload);
   }
 }

Enter fullscreen mode Exit fullscreen mode

You can use @Throws decorator to explicitly tell which type of exception we've to expect in order to trigger specific responses to the client. Mind blowing, right?

See how we're exporting custom errors from our model layer src/models/user.model.js:

     if(requiredProps.every(prop => props.includes(prop))) {
       return Promise.resolve();
     } else {
-      return Promise.reject();
+      throw new UserValidationError
     }
   }

@@ -29,7 +28,7 @@ export default class UserModel {

   isUnique({ mail }) {
     return new Promise((resolve, reject) =>
-      this.database.exists("users", { mail }).then(reject, resolve));
+      this.database.exists("users", { mail }).then(() => reject(new UserMailInUseError), resolve));
   }

   searchBy(predicate) {
@@ -48,3 +47,15 @@ export default class UserModel {
     return this.database.update("users", { uid }, { mail, name });
   }
 }
+
+export class UserValidationError extends Error {
+  constructor() {
+    super("missing fields, required: [name, mail, pass]")
+  }
+}
+
+export class UserMailInUseError extends Error {
+  constructor() {
+    super("mail is already taken, try another one")
+  }
+}
Enter fullscreen mode Exit fullscreen mode

So @Throws(errorType, responseFn) just goes beyond. While @Catch will handle any exception regardless of error type, @Throws just provides more concrete way to handle out http layer.

This is the final look for now on src/resources/user.resource.js:

import { AbstractResource } from "@ritley/core";
import UserModel, { UserValidationError, UserMailInUseError } from "../models/user.model";

import {
  Dependency,
  ReqTransformBodyAsync,
  Default,
  Throws,
  InternalServerError,
  BadRequest,
  Conflict,
  Created
} from "@ritley/decorators";

@Dependency("userModel", UserModel)
export default class UserResource extends AbstractResource {
  constructor() {
    super("/users");
  }

  @Throws(SyntaxError, BadRequest)
  @Throws(UserValidationError, BadRequest)
  @Throws(UserMailInUseError, Conflict)
  @Default(Created)
  @ReqTransformBodyAsync
  async post(req, res) {
    const body = await req.body;
    const payload = body.toJSON();
    await this.userModel.validate(payload);
    await this.userModel.isUnique(payload);
    return this.userModel.create(payload);
  }
}
Enter fullscreen mode Exit fullscreen mode

Just to recap. Whether to use @Throws or @Catch is up to you although @Catch can be considered as a @Throws(Error, fn) alias because it will be executed on any exception. But @Throws is more deterministic because you can tie your HTTP responses to specific kind of errors or success.

Basically all the framework logic is on http layer. Models are completely independent despite providers.

ritley defines http triggers that will invoke specific operations on the model that, either success or fail, will be handled back using a declarative set of expressions on top of the same trigger. This basically allows for non-intrusive and declarative development on the backend.

That's all for now folks! Following chapter on series will be about handling sessions, separation of concerns and to keep things scalable with ritley. Cya!

ritley from metroid

Top comments (15)

Collapse
 
avalander profile image
Avalander • Edited

I'm with Theofanis here, your code looks a lot more like the code I wrote back in the days when I developed web services with Java and Spring than any node app I've ever seen. Be far from me to tell you how you should or shouldn't write your code, but I have to ask: what about your code is idiomatic Javascript?

Collapse
 
k1r0s profile image
Ciro Ivan

Hey! thanks for your comment. To me, compare my code with Spring is more than a compliment than a bad thing. But I have to point out that this framework is far far simpler. Library core is less than 80 lines, adding the extensions probably 170.

To me idiomatic code is a piece of code that doesn't need any inline comments or explanations to understand whats going on, as long as you've knowledge on the language it self.

This article is part of a series so this development has not concluded yet. But try to do the same app with express (full app) following requirements and then tell me which one is more idiomatic. Deal?

Collapse
 
avalander profile image
Avalander

It sounds like we don't share the same definition for idiomatic, then. The way I see it, idiomatic code is not only easy to grasp, but it follows the common conventions and constructs of the language and takes advantage of its expressive (or lack thereof) power. I really like the definition Corey Goldberg gives in this SO question:

Idiomatic means following the conventions of the language. You want to find the easiest and most common ways of accomplishing a task rather than porting your knowledge from a different language.

I might take on your challenge if I manage to find some free time, but I'm afraid we both would end up preferring our own code for obvious reasons. Anyway, I don't think my own Javascript code is always very idiomatic because I tend to use a lot of functional constructs more common in other languages, so... shrug.

Thread Thread
 
k1r0s profile image
Ciro Ivan

For me, a language should be easy to grasp or understand. That's the most important goal.

Thread Thread
 
k1r0s profile image
Ciro Ivan

Anyways languages tend to evolve and find shortcuts. JavaScript for example has been evolving since its creation that means drop some features to use new ones. Its like you're telling me that a language is something metaphysical rather than a tool that should fit or needs..

Of course each of us will prefer what is best for our experience or knowledge. Thats why languages evolve when new comers use it. Since 2009 JavaScript has changed a lot because of new comers from languages like Java and c# (.NET). People back on the days was comfortable with prototypes and callbacks. That was the:

the easiest and most common ways of accomplishing a task

I don't mind abot the challenge, use what fit best for you. But I don't get the point of this discussion.

Thread Thread
 
avalander profile image
Avalander

Its like you're telling me that a language is something metaphysical rather than a tool that should fit or needs

I beg your pardon? I never meant anything remotely similar to that. All I said is (a) your code looks more like idiomatic Java than Javascript to me; (b) so what makes your code idiomatic Javascript?; and (c) oh, we define idiomatic differently, that would explain my confusion. I'm sorry if it came as me somehow challenging your code.

However, if we would discuss what idiomatic Javascript (by my definition of idiomatic) is, then we would definitely enter the realm of philosophy and metaphysics and should abandon any hopes of ever reaching a satisfactory conclusion. Mostly because the language has evolved without a unified vision and there are several interested groups trying to stir the evolution of the language in the direction they find most appealing; and there are as many opinions in that regard as developers, and no authority that can be reliably quoted.

But now I digress...

But I don't get the point of this discussion.

The point of this discussion appears to be that we have different definitions for idiomatic and I wasn't aware of that, therefore I asked. Clarified that point, however, I see no reason for any further discussion either.

Thread Thread
 
k1r0s profile image
Ciro Ivan

To me, writing an article titled "potatoes are rounded" and somebody replies to it asking "why you think potatoes are rounded" which was previously being showed (as my opinion) is either trying to discredit or challenging my point or view, which for me is fair but I think is beyond scope.

First being compared to spring (I get it as a bad thing, that's what it seems to me) and being asked "what about your code you find idiomatic" well.. even if we have different opinions about what means idiomatic: we can disagree about sky's color (sunny day), but we cannot disagree on that being similar to the sea. As a matter of fact I beg you to compare building the same app features on top another library or framework you choose.

I prefer to discuss about concrete things. Even if we disagree it would be enlighten to the community.

This article, as part of series, aren't fully concluded, this is not the full picture. If you're interested on more in deep information about the library then, please go check it github.com/k1r0s/ritley

Thread Thread
 
k1r0s profile image
Ciro Ivan • Edited

The whole point of the article:

what makes your code idiomatic

I think my code here, on this tutorial is easy/simple to grasp, follow, understand, etc

Thread Thread
 
avalander profile image
Avalander

As I said, to me idiomatic code is code that follows the design principles of the language and the common patterns and constructs native to the language that the community has identified as most useful to write succinct and expressive code. You seem to define idiomatic code as just good code. The way I see it, idiomatic code should result in good code most of the time, but it doesn't always, and good code isn't necessarily idiomatic.

Thus, when I read a title saying idiomatic Javascript, I understand that you are saying this code is a good example of how Javascript should generally be written because it uses the patterns native to the language that have been tested and deemed most useful to write expressive code across many applications.

From this point of view, claiming that anything is idiomatic Javascript is a rather bold statement, because Javascript is a chimera that has evolved in the recent years to please everyone without a unified vision of how the language should look like or be used, and there is no central authority that either of us can quote to support what we consider idiomatic, like we would have in Python, for instance.

At this point, I think discussing any further is pointless because it's clear that we don't think about idiomatic code in the same way and you've stated that you're not interested in such philosophical discussions. However, if you must know, here's why I'm surprised that you define your code as idiomatic:

It makes heavy usage of decorators. Apparently, most of your framework is based on adding behaviour to classes through decorators. Decorators are not part of the Javascript standard yet, and I don't think you can claim any code that heavily uses a construct that is not in the language, and needs to be transpiled to even be valid runtime code, to be idiomatic. It can be good code, it can be a nice approach, but it is definitely not idiomatic.

As a result of your heavy usage of decorators, almost everything that interacts with your framework is a class, because decorators can't be used outside classes. No object literals, no encapsulation via closures and partial application, no functional mixins... Javascript offers much more than classes and your code doesn't take advantage of it because it's heavily based on a feature that's not even in the language yet.

To reiterate, I'm not saying that your code is not good, I'm not saying that you should write it in a different way, and you have written your own web framework, which I haven't, so kudos for that. But I have trouble seeing it as idiomatic Javascript.

Thread Thread
 
k1r0s profile image
Ciro Ivan

Thanks for the info tho!

Thread Thread
 
k1r0s profile image
Ciro Ivan

btw, the previous version of the same library was written without any ES6 classes or decorators: github.com/k1r0s/ritley-alpha

to me, it seems more to angularjs, what do you think?

Collapse
 
theodesp profile image
Theofanis Despoudis

Are you sure you are using the right language here? It reminds me of Java ...

Collapse
 
k1r0s profile image
Ciro Ivan

Good, Java has annotations (kind of decorators). I don't know if you think if this is good or bad. To me decorators are a powerful tool. Many frameworks make hard use of them.

Yep, I'm using the right language here.

Collapse
 
theodesp profile image
Theofanis Despoudis

Decorators are harder to use than misuse. Plus they are experimental (stage 2) so expect some debates against them.

Thread Thread
 
k1r0s profile image
Ciro Ivan

Discussion is only on implementation details, they're unlikely to remove any of its features as I see on tc39 threads, even adding more.

Decorators are very easy to use if you know to do that. I think you're a bit influenced by your past experience with Java annotations. I know, that thing was a nightmare.

I do find that the problem was some design failures by framework it self (spring) rather than the idea about use decorators to encapsulate behaviors.