DEV Community

Ciro Ivan
Ciro Ivan

Posted on

Idiomatic JavaScript Backend. Part 3

Hi everyone! This part of series Idiomatic JavaScript Backend.

Part 1/3
Part 2/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

6. Creating and managing sessions

ritley doesn't offer an orthodox way to handle sessions.

The End!

Not kidding.. yeah, well but what ritley does best is to provide you tools to drop behaviors and requirements from method to method, modules and even projects.

Now, for the rest of the requirements such as showing a list of users or updating an user need clients to allow creating sessions by using its credentials.

Hence lets create a couple of users if you don't have any!

$ curl -d '{ "name": "Randell Kovalsky", "mail": "rk@foo.b", "pass": "asd123"}' localhost:8080/users

Create few users by using previous command, lets say 3 or 4 users its enough.

Now, remember about src/resources/session.resource.js? time to add a feature:

import { AbstractResource } from "@ritley/core";
import SessionModel, { SessionInvalidCredentialsError } from "../models/session.model";

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


@Dependency("sessionModel", SessionModel)
export default class SessionResource extends AbstractResource {
  @Default(MethodNotAllowed) get() {}
  @Default(MethodNotAllowed) put() {}
  @Default(MethodNotAllowed) delete() {}

  constructor() {
    super("/sessions");
  }

  @Throws(SyntaxError, BadRequest)
  @Throws(SessionInvalidCredentialsError, Unauthorized)
  @Default(Created)
  @ReqTransformBodyAsync
  async post(req) {
    const body = await req.body;
    const payload = body.toJSON();
    const user = await this.sessionModel.validateCredentials(payload);
    return this.sessionModel.upsertSession(user);
  }
}
Enter fullscreen mode Exit fullscreen mode

Lets review the requirements along with previous snippet:

  • post method should create a session by identifying the client through user credentials
  • validateCredentials just return the user from the database by comparing mail and password (encrypted).
  • upsertSession method creates or updates the session. It always receive an user and returns a session with an expiration date of +30 minutes.
  • get, put and delete methods must be explicitly rejected with HTTP 405
  • file proper responses on each case: success, wrong json on payload, wrong credentials, errors during session creation.

You probably realized about we duplicated code on parsing payload from src/resources/user.resource.js. This is just temporal, we'll fix later on.

And we've added a dependency by the name of sessionModel where specific non-transporter-layer logic should be placed such as creating, updating, managing expiration, credential validation... I'm not going to paste but you can check the code here.

So now, running the project again and executing this command on the term:

$ curl -d '{ "mail": "<mail>", "pass": "<pass>"}' localhost:8080/sessions

Now the server answers you with something like this:

{"uid":"TjVNZy8yk","expiration":1535293179182,"userUid":"xqR16Gi7w"}

Well, here sessions are very simple and unorthodox. Successful authentication is a json with the uid of the session, an expiration timestamp and the associated user's uid as userUid.

As defined on requirements to use that newly created session the client should provide a header -H "x-session: <session_uid>" on every call that requires a session.

For instance if we would list all registered users on the app we should do this:

$ curl -H "x-session: TjVNZy8yk" localhost:8080/users

Now we have to modify our src/resources/user.resource.js to fulfill GET call:

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

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

 @Dependency("userModel", UserModel)
+@Dependency("sessionModel", SessionModel)
 export default class UserResource extends AbstractResource {
   constructor() {
     super("/users");
@@ -23,11 +26,21 @@ export default class UserResource extends AbstractResource {
   @Throws(UserMailInUseError, Conflict)
   @Default(Created)
   @ReqTransformBodyAsync
   async post(req) {
     const body = await req.body;
     const payload = body.toJSON();
     await this.userModel.validate(payload);
     await this.userModel.isUnique(payload);
     return this.userModel.create(payload);
   }
+
+  @Throws(SessionNotCreatedError, Unauthorized)
+  @Throws(SessionExpiredError, Unauthorized)
+  @Default(Ok)
+  async get(req) {
+    const uid = req.headers["x-session"];
+    const session = await this.sessionModel.sessionExists({ uid });
+    await this.sessionModel.revalidate(session);
+    return this.userModel.searchBy();
+  }
 }
Enter fullscreen mode Exit fullscreen mode

As you can see we've just added get method.

Now users are able to:

  • create users and define credentials
  • create a session using credentials
  • list users using a session

Although there are some issues here:

  • user.resource.js contains code that handles calls from sessionModel which is not explicitly related. though is relation is implicit by business rules. Se need to address that relationship more implicit. Because now involves mixing different domain concerns.

  • Also, sessions management may involve errors that aren't related to users too.

  • Next development cycles may include new resources, lets say /cities or something are we going to copy and paste code in order to manage sessions, payload parsing and all its error specific handling? (many people actually do)


7. Separation of concerns

How to address the problem with replication?

Old school OOP programmers will try to group shared behaviors on upper classes and then, place specific behaviors as a subclasses but that's hard to scale, too many strong relationships, complex code bases.

On the other hand FP programmers will create a pipe of middlewares (req, res, next) that doesn't allow too much for building abstractions that can become something known as middleware hell. Functions are monoliths, to a certain point, in general, it doesn't scale quite well due to wrong encapsulation and side-effects problems.

I'm not here to argue against paradigms, I'm just trying to generally assess enterprise wide used solutions such as Spring, .NET and expressJS by its coding banner.

My point is that every paradigm or technique is good, but it doesn't make sense to use it to solve all problems like previous frameworks did.

Both paradigms defined above doesn't avoid you to explicitly call them and hence deal with duplication at some point. Completely get rid of duplication is impossible though but, problem with duplication isn't solely the amount of code that you have to paste all across your code base... is about dealing with lines of code that involve calls, assignments, sending arguments... more code, more to read, track, test, maintain, etc. By reading some method's code how you can tell if all code placed is related to the main concern? for instance:

increasePrice(amount) {
  ...requiredStuff1(amount)
  ...callAnotherService(this)
  ...etc
  this.price += amount;
  ...logThatPriceCorrectlyIncreases(this.price)
}
Enter fullscreen mode Exit fullscreen mode

How many times you read some piece of code and then asked: What this code is really doing?, haven't you?

Lets try to decouple shared behaviors into something more portable and declarative.

Going back to our project, lets illustrate this by coding what I think is the solution to this problem: both src/resource/user.resource.js and src/resource/session.resource.js need to receive body payload, actually the do it by using @ReqTransformBodyAsync, but we still need to actually parse the body string to JSON and handle errors if any.

Would be nice if @ReqTransformBodyAsync solve the entire problem, right? then we would be able to put our wonderful decorator on every single method that needs that chunk of behavior when we actually need it. We don't want to call app.use().

Of course @ritley/decorators doesn't know what kind of stuff we need to do regarding on error handling, every project is different, you may need to call a Logger, who knows. It only provides request buffer concatenation which is the most basic that a library can do for you!

But, it allows you to extend decorators. Lets create a file on src/decorators/req-body-json.decorator.js:

import { beforeMethod } from "kaop-ts";

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

function parseRequestBody(meta) {
  const [req, res] = meta.args;
  req.body.then(body => {
    try {
      const payload = body.toJSON();
      meta.commit(payload);
    } catch (e) {
      BadRequest(res, e.message);
    }
  })
}

export default beforeMethod(
  ...ReqTransformBodyAsync.advices(),
  parseRequestBody
)

Enter fullscreen mode Exit fullscreen mode

wat-decorators

Well, first we need to understand what @ReqTransformBodyAsync actually do:

creates a promise that will resolve when Request payload will be fully received and it assigns it to req.body.

We're just extending this decorator by adding a behavior right after first one concludes.

Hence this new decorator contains the behavior of @ReqTransformBodyAsync and our custom function that basically tries to parse request payload into a json, if there is an error it will file a HTTP 400 Bad Request, if succeeds it will execute the method that is decorating with an additional parameter which resolves to the json's payload.

Don't panic if this sounds like Spanish for you. Let me show you this:

 import { AbstractResource } from "@ritley/core";
 import SessionModel, { SessionInvalidCredentialsError } from "../models/session.model";
+import ParseReqBody from "../decorators/req-body-json.decorator";

 import {
   Default,
@@ -7,9 +8,7 @@ import {
   Created,
   Throws,
   Unauthorized,
   BadRequest,
   Dependency,
-  ReqTransformBodyAsync
} from "@ritley/decorators";


@@ -25,14 +24,10 @@ export default class SessionResource extends AbstractResource {
     super(SessionResource.URI);
   }

-  @Throws(SyntaxError, BadRequest)
   @Throws(SessionInvalidCredentialsError, Unauthorized)
   @Default(Created)
-  @ReqTransformBodyAsync
+  @ParseReqBody
-  async post(req) {
+  async post(req, res, payload) {
-    const body = await req.body;
-    const payload = body.toJSON();
     const user = await this.sessionModel.validateCredentials(payload);
     return this.sessionModel.upsertSession(user);
   }
 }
Enter fullscreen mode Exit fullscreen mode

That means both src/resources/user.resource.js and src/resources/session.resource.js will use it, so we can get rid of SyntaxError check too.

Now payload requirement is defined outside of the method code, like a dependency, but rather than a service, you're injecting a behavior. This is like middlewares on steroids.

Wonder that you can declare @ResolveUploadedFile("/path/:filename") on top of some method and, in the background, receive a request with a multiplart/form-data payload (a file), save the file into a predefined route, etc, and after all execute the method that you decorated like if nothing really happened? Yes, we can.

Lets create a decorator called @ValidateSession to clean up our src/resources/user.resource.js from non-related user domain:

 import { AbstractResource } from "@ritley/core";
 import UserModel, { UserValidationError, UserMailInUseError } from "../models/user.model";
-import SessionModel, { SessionNotCreatedError, SessionExpiredError } from "../models/session.model";
+import ParseReqBody from "../decorators/req-body-json.decorator";
+import ValidateSession from "../decorators/validate-session.decorator";

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

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

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

-  @Throws(SessionNotCreatedError, Unauthorized)
-  @Throws(SessionExpiredError, Unauthorized)
   @Default(Ok)
+  @ValidateSession
-  async get(req) {
+  get(req) {
-    const uid = req.headers["x-session"];
-    const session = await this.sessionModel.sessionExists({ uid });
-    await this.sessionModel.revalidate(session);
     return this.userModel.searchBy();
   }
 }

Enter fullscreen mode Exit fullscreen mode

And now, remember that code related with the session management? We moved this to a proper separate location. We've created another folder:

src/
├── config
│   ├── database.config.js
│   └── lowdb.json
├── decorators
│   ├── req-body-json.decorator.js
│   └── validate-session.decorator.js
├── index.js
├── models
│   ├── session.model.js
│   └── user.model.js
├── resources
│   ├── session.resource.js
│   └── user.resource.js
└── services
    ├── database.service.js
    └── encrypt.service.js
Enter fullscreen mode Exit fullscreen mode

To recap, session and user resources both share payload management, so we've created a decorator that encapsulates required behavior to parse req body and then we've defined on both resources. We did similar with session requirement since next feature edit users will rely on it.

So, as you can see, ritley provides OO standards in order to deal with basic architecture and separation of concerns but enhances it with FP extensions as middleware decorators that can be plugged before a method, after a method, if method throws an error. So that's pretty neat.


8. Everybody's concern, scalability

Do I need to explain you how to write scalable apps? If you are still here you might know a few tips on this. Isn't solely a matter of which tools you choose, it certainly impact but most of times is all about decisions that you did… or indeed the framework did.

ritley only took decisions on http/transport layer. That means you are the only one to blame on other concerns. it scares right? Some might see it as a redemption.

As you saw on previous parts during this tutorial, plain, simple, well understood architectures empowers developers to write better code.

Now we need to add the last feature, user edition. Lets add a PUT method handler on src/resources/user.resource.js:

@Throws(UserInsufficientPermError, Forbidden)
@Default(Ok)
@ValidateSession
@ParseReqBody
@ReqTransformQuery
put(req, res, session, payload) {
  return this.userModel.putUser(req.query.uid, session.userUid, payload);
}
Enter fullscreen mode Exit fullscreen mode

Have you noticed that request payload will not be parsed if user doesn't have a valid session? decorator order does matter :D, simply because it goes before [top-to-down].

That's all we've to do on our http layer.

Note that, we are calling putUser on src/models/user.model.js. Lets see whats new here:

@@ -43,6 +43,14 @@ export default class UserModel {
     }
   }

+  isAllowedToEdit(requestedUserUid, currentUserUid) {
+    if(requestedUserUid === currentUserUid) {
+      return Promise.resolve();
+    } else {
+      return Promise.reject(new UserInsufficientPermError);
+    }
+  }
+
   update(uid, { mail, name }) {
     return this.database.update("users", { uid }, { mail, name });
   }
@@ -50,6 +58,10 @@ export default class UserModel {
   postUser(payload) {
     return this.validate(payload).then(() => this.isUnique(payload).then(() => this.create(payload)));
   }
+
+  putUser(requestedUserUid, currentUserUid, payload) {
+    return this.isAllowedToEdit(requestedUserUid, currentUserUid).then(() => this.update(requestedUserUid, payload));
+  }
 }

 export class UserValidationError extends Error {
@@ -63,3 +75,9 @@ export class UserMailInUseError extends Error {
     super("mail is already taken, try another one")
   }
 }
+
+export class UserInsufficientPermError extends Error {
+  constructor() {
+    super("you don't have permissions to perform this action")
+  }
+}
Enter fullscreen mode Exit fullscreen mode

That means an user can only update its own profile.

Lets try this by running a curl command:

$ curl -X PUT -H "x-session: <session_uid>" -d '{ "name": "Jean-Luc Godard"}' localhost:8080/users?uid=<target_user_uid>

You get back either a verbose error or newly updated user.

You may noticed to handle querystring parameters and access req.query we've added @ReqTransformQuery on put method. Now you may be asking: "Do I have to add a decorator for every single case?". If you're building a more complex application you probably need to define a more complex class base, instead of pile 7 decorators per method you might extend your resources from MyAbstractResource rather of generic ritley's AbstractResource. You may need to build a framework on top of this LMAO.

This chapter is completed. Now users can change their names! (...)

Some tips on scalable software:

As always, try to scale horizontaly, avoid more than two levels of inheritance on classes, remember that you can extend decorators that fit best your case, etc.

For instance previous feature we just added on edit users only involved additions on the commit, we didn't change previous code. Thats the gold rule for scalability.

Any chunk of code non-related to any feature should be easy to extend, but not suited for modification.

You should not try to abstract your business logic since you don't even known whats going next. As a programmer you need to represent business logic as it is and deal with infrastructure code with abstraction and modular approaches. That is what defines software quality.

For example our specific decorators that target session management defines a closed design which can be easily extended since most of business logic is defined on models and decorator it self only provides the glue to attach it into classes.

Common problems related with scalability like huge code changes of unrelated domains are due to bad/closed design when you cannot access this service from here because there is another service that is doing nasty stuff on the background.. most likely due to miss placed responsibility.

Your job is always to keep concerns on separated layers.


9. Conclusion

ritley its a very simple concept. It was created 9 months ago but completely rewrited to be released as OSS. The basic concept is to provide common patterns on transport layer without wrapping nodejs documentation. In fact it only dispatch request to the proper handler by mapping your classes, hence its quite fast, well tested, and easy to master since you don't need to learn anything that you probably known if you're a casual node developer.

Library core is less than 80 lines (at the time I'm writing this) and likely will remain quite simple. Though library extension @ritley/decorators is about 200 lines. Even though is still small in comparison to other frameworks.

Congratulations comrade. Did you enjoy the article? let me know your thoughts down below or lets chat on twitter :) thank you ❤

Did you like the idea about the library? Do you want to contribute? I'm always open to new ideas!

ritley from metroid

Top comments (1)

Collapse
 
annlin profile image
ann lin

NICE OSS! Love 0 dependency!