DEV Community

Ciro Ivan
Ciro Ivan

Posted on • Updated on

Idiomatic JavaScript Backend. Part 1

Hi everyone! This part of series Idiomatic JavaScript Backend.

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

0.What

Hi everyone! today's topic is about building an App with NodeJS.

What we're gonna do? We will build a service for allowing users to:

  • create its own profile
  • create a session
  • list other users
  • edit its own user

And…

We're going to use cURL!

Its not relevant to check, but you can click here to see the full requirements on what this app should fulfill.

Now I'm going to slowly build it from scratch!


1. Preparing the environment

Let's do our "Hello World" with ritley to get started:

.
├── .babelrc
├── package.json
└── src
    └── index.js
Enter fullscreen mode Exit fullscreen mode

In this tutorial we're going to use Babel. To do so with nodejs we need babel-node to run our app. So this is our package.json:

{
  "name": "tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "babel-node src"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@ritley/core": "^0.3.3",
    "@ritley/standalone-adapter": "^0.2.0",
  },
  "devDependencies": {
    "@babel/core": "^7.0.0-beta.55",
    "@babel/node": "^7.0.0-beta.55",
    "@babel/plugin-proposal-class-properties": "^7.0.0-beta.55",
    "@babel/plugin-proposal-decorators": "^7.0.0-beta.55",
    "@babel/plugin-transform-async-to-generator": "^7.0.0-rc.1",
    "@babel/preset-env": "^7.0.0-beta.55"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why @ritley/core and @ritley/standalone-adapter ? :|

As ritley is quite small, many features are separated on different packages. As core is indeed required, standalone adapter too because we're going to run a node server by ourselves here. If you're on serverless environments such as firebase you can keep going without it.

This would be our .babelrc:

{
  "presets": [["@babel/preset-env", {
    "targets": {
        "node": "current"
      }
    }]],
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", { "loose": false }],
    ["@babel/plugin-transform-async-to-generator"]
  ]
}
Enter fullscreen mode Exit fullscreen mode

And our hello world src/index.js:

import { setAdapter, AbstractResource } from "@ritley/core";
import Adapter from "@ritley/standalone-adapter";

setAdapter(Adapter, {
  "port": 8080
});

class SessionResource extends AbstractResource {
  constructor() {
    super("/sessions");
  }

  get(req, res) {
    res.statusCode = 200;
    res.end("Hello from sessions!");
  }
}

class UserResource extends AbstractResource {
  constructor() {
    super("/users");
  }

  get(req, res) {
    res.statusCode = 200;
    res.end("Hello from users!");
  }
}

new SessionResource;
new UserResource;

Enter fullscreen mode Exit fullscreen mode

In previous snippet we import standalone-adapter and we bind it to the core by calling setAdapter(<adapter> [, <options>]). This will create and bind a new HttpServer to any AbstractResource subclass. You can check how it works.

When building a ritley app you've to choose an adapter. That defines how requests are sent to resources.

ritley uses https://nodejs.org/api/http.html (req, res)api so probably you're quite familiar with it.

Note that we've created two similar classes, we could do this instead:

import { setAdapter, AbstractResource } from "@ritley/core";
import Adapter from "@ritley/standalone-adapter";

setAdapter(Adapter, {
  "port": 8080
});

class DefaultResource extends AbstractResource {
  get(req, res) {
    res.statusCode = 200;
    res.end(`Hello from ${this.$uri}`);
  }
}

new DefaultResource("/sessions");
new DefaultResource("/users");

Enter fullscreen mode Exit fullscreen mode

Anyways we're going to keep it separated as both resources will start diverge quite soon.

now you can $ npm start and then run some curl commands to see if everything is working properly:

$ curl localhost:8080/users
$ curl localhost:8080/sessions

This is our first step!


2. Connecting a persistence layer

We need to have some kind of persistence layer. We're going to install lowdb because we don't need too much overhead for now.

Everyone favorite part: its time to install new dependencies!:

$ npm install lowdb shortid

However we need to keep in mind that any dependency, whatever we attach to our project, should be easy to replace. That's we're going to wrap lowdb into an interface with "CRUD alike" methods to keep things extensible.

Lets continue by implement our database.service.js using lowdb:

import low from "lowdb";
import FileAsync from "lowdb/adapters/FileAsync";
import config from "./database.config";
import shortid from "shortid";

export default class DataService {
  onConnected = undefined

  constructor() {
    this.onConnected = low(new FileAsync(config.path, {
      defaultValue: config.defaults
    }))
  }

  create(entity, newAttributes) {
    return this.onConnected.then(database =>
      database
      .get(entity)
      .push({ uid: shortid.generate(), ...newAttributes })
      .last()
      .write()
    )
  }
}

Enter fullscreen mode Exit fullscreen mode

For now we only implement create method. That's fine now.

.
└── src
    ├── database.config.js
    ├── database.service.js
    ├── index.js
    └── lowdb.json
Enter fullscreen mode Exit fullscreen mode

Our project is growing fast! We've created database.config.js too which contains important data that may be replaced quite often so we keep it here:

export default {
  path: `${__dirname}/lowdb.json`,
  defaults: { sessions: [], users: [] }
};
Enter fullscreen mode Exit fullscreen mode

You can skip this paragraph if you've already used lowdb. Basically you need to specify the actual path of the physic location of the database, since it doesn't need a service like other database engines. Hence lowdb is way simpler and fun to play with, though less powerful and should not be used to build enterprise projects. That's why I'm wrapping the whole lowdb implementation on a class that exposes crud methods, because its likely to be replaced anytime.

And now, we've changed our src/index.js to properly connect database to controllers:

@@ -1,5 +1,6 @@
 import { setAdapter, AbstractResource } from "@ritley/core";
 import Adapter from "@ritley/standalone-adapter";
+import DataService from "./database.service";

 setAdapter(Adapter, {
   "port": 8080
@@ -17,15 +18,18 @@ class SessionResource extends AbstractResource {
 }

 class UserResource extends AbstractResource {
   constructor() {
     super("/users");
+    this.database = new DataService;
   }

-  get(req, res) {
-    res.statusCode = 200;
-    res.end("Hello from users!");
+  post(req, res) {
+    this.database.create("users", { name: "Jimmy Jazz" }).then(user => {
+      res.statusCode = 200;
+      res.end(JSON.stringify(user));
+    });
   }
 }

 new SessionResource;
 new UserResource;

Enter fullscreen mode Exit fullscreen mode

We've changed as well our get method to a post to emulate a real case of creation request. By running this command we get back the newly created data!

$ curl -X POST localhost:8080/users

Check src/lowdb.json to see the changes!

Okay so, we just connected lowdb and run our first insertion!


3. Improving the project structure

We need to organize a bit our project.

First we're going to arrange our folders like this:

// forthcoming examples will only show src/ folder
src/
├── config
│   ├── database.config.js
│   └── lowdb.json
├── index.js
├── resources
│   ├── session.resource.js
│   └── user.resource.js
└── services
    └── database.service.js
Enter fullscreen mode Exit fullscreen mode

Now lets remove a bit of code from src/index.js in order to have only the following:

import { setAdapter } from "@ritley/core";
import Adapter from "@ritley/standalone-adapter";

import SessionResource from "./resources/session.resource"
import UserResource from "./resources/user.resource"

setAdapter(Adapter, {
  "port": 8080
});

new SessionResource;
new UserResource;
Enter fullscreen mode Exit fullscreen mode

So basically we moved our controllers (aka resources) to a separated folder called resources.

Next is to setup Dependency Injection on src/resources/user.resource.js to be able to inject an instance of our database service.

In order to do so we're going to install an extension package called @ritley/decorators:

$ npm install @ritley/decorators

Then, lets make a few changes on src/services/database.service.js to be exported as a singleton provider:

 import config from "../config/database.config";
+import { Provider } from "@ritley/decorators";

+@Provider.singleton
 export default class DataService {
   onConnected = undefined
Enter fullscreen mode Exit fullscreen mode

By adding @Provider.singleton we will be able to construct only one instance every time the provider gets executed. That means all classes that declare it as a dependency will share the same instance.

Lets add it to src/resources/user.resource.js:

 import DataService from "../services/database.service";
+import { Dependency, ReqTransformBodySync } from "@ritley/decorators";

+@Dependency("database", DataService)
 export default class UserResource extends AbstractResource {
   constructor() {
     super("/users");
-    this.database = new DataService;
   }

+  @ReqTransformBodySync
   post(req, res) {
+    const payload = req.body.toJSON();
+    this.database.create("users", payload).then(user => {
-    this.database.create("users", { name: "Jimmy Jazz" }).then(user => {
       res.statusCode = 200;
Enter fullscreen mode Exit fullscreen mode

@Dependency executes DataService (now its a provider) then receives an instance and assigns it as a named property after class local constructor gets executed.

So basically we removed complexity that involves service instantiation on controllers. I guess you're familiar with these practices.

You may noticed that we've also removed hardcoded payload and we've placed @ReqTransformBodySync on top of the post method.

This decorator allows to access request body or payload by delaying method execution till its fully received. Like body-parser does but more explicit because you don't need to bother yourself reading method contents to know that it requires payload to properly work, and its more pluggable since you can configure at method level.

Now try to execute this command:

$ curl -d '{ "name": "Pier Paolo Pasolini" }' localhost:8080/users

-X POST is assumed if -d (payload) is provided.

You should reveive a HTTP 200 OK response with ur new user created! Check database contents :)

That's all for now folks! On next chapter on series we will see how ritley manages to link models with controllers, handle exceptions and manage sessions.

ritley from metroid

Discussion (6)

Collapse
aralroca profile image
Aral Roca • Edited

I didn't know about the existence of lowdb before! Also I like a lot the idea of these decorators and the abstract class from ritley. By the way, are you the owner of ritley? Very good job! Great contribution!

Collapse
k1r0s profile image
Ciro Ivan Author

And yes, I've created the framework 9 months ago for some projects. Now I rewrite completely to release as OSS. but is far powerful than this :)

Collapse
k1r0s profile image
Ciro Ivan Author

Yeah, lowdb is neat for development, later you can replace with other much stronger db.

Collapse
k1r0s profile image
Ciro Ivan Author

It does more than that! It's a whole backend framework. I'll post more content on the weekend :)