Hi everyone! This part of series Idiomatic JavaScript Backend.
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
Go to specific tag
$ git checkout 1.preparing-the-env
Go to latest commit
$ git checkout master
See differences between tags on folder src
$ git diff 1.preparing-the-env 2.connecting-a-persistance-layer src
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
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"
}
}
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"]
]
}
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;
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");
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()
)
}
}
For now we only implement create
method. That's fine now.
.
└── src
├── database.config.js
├── database.service.js
├── index.js
└── lowdb.json
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: [] }
};
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;
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
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;
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
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;
@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.
Top comments (4)
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!
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 :)
Yeah, lowdb is neat for development, later you can replace with other much stronger db.
It does more than that! It's a whole backend framework. I'll post more content on the weekend :)