Threerest is a light and powerful framework for creating hypermedia API for node. For the moment, only HAL concept is implement for level 3.
Threerest is a npm module so you have just add it to your project like this :
if npm < 5
npm install threerest --save
if npm > 5
npm i threerest
or manually add the dependency in package.json
Threerest is base on the ES7 decorator. You just have to put a service decorator on a class to transform into service REST. To create a method, you can use the method decorators and if you want to make your service a Hypermedia Service, you must add the Hal decorator, it's so simple. This is an example, it's more telling :
import { Service } from "threerest";
import { Methods } from "threerest";
import { Hal } from "threerest";
@Service.path("/authors") // To set a part of the path of the API
export default class ServiceAuthors {
@Methods.get("/") // To set the verb
@Hal.halServiceMethod() // To add hypermedia
getAll() {
.....
}
}
If you don't need hypermedia, threerest can do it. You must just mark the class with service decorator, the methods with http verb method decorators and use service utility for load router in express.
import { Convert, Methods, Service } from "threerest";
import Param from "./param";
@Service.path("/test")
export default class ServiceTest {
@Method.get("/:id")
@Convert(Param)
testGet(value) {
return value;
}
}
This decorator take only one parameter and he must place on a class. the parameter is the basic path for the service.
@Service.path("/test")
In the example, the service can be execute with the url http:\\localhost:1234\test
For the moment, only GET, POST, DELETE, PUT and PATCH are implements
@Method.get("/:id")
In the example, the service method will be execute using the url http:\\localhost:1234\test\12
. Without convert, the method will call with :
- request parameters
- request
- response
With convert, the method will call with :
- convert object with the request parameters
- request
- response
Using for convert the parameter(s) into particular JavaScript object. In the example, we convert the request parameter, into Param object :
export default class Param {
id = undefined;
name = "";
constructor(name, password) {
this.name = name;
this.password = password;
}
};
And the first parameter method contain an object Param
with the property id witch contain the value of the request parameter.
@Service.path("/three")
export default class ServiceManageStatus {
@Method.get("/:id")
@convert(Param)
testGet(value) {
value.method = "get"
return value;
}
}
In this example, the parameter value will be type of Param
, and his id attribut will be set with the request parameter exctract from the URL.
For secure some methods service, use the 'secure' decorator. He take the user from the request and compare the roles property on this one with roles parameters passed to the decorator.
A service that allow call testGetUser
for USER role and testGetAdmin
for USER or ADMIN role :
@Service.path("/one")
export default class ServiceTest {
@Method.get("/user/:id")
@convert(Param)
@Secure.secure(["USER"])
testGetUser(value) {
return value;
}
@Method.get("/adminuser/:id")
@convert(Param)
@Secure.secure(["USER", "ADMIN"])
testGetAdmin(value) {
return value;
}
}
You just must have a middleware for write user property on the request. This example parse a token JWT that contain the user
/* Express middleware for extract user from JWT Token */
function jwtMiddleWare(request, response, next) {
if (request && request.get && request.get(Secure.HEADER_AUTH) && request.get(Secure.HEADER_AUTH).slice(0, Secure.BEARER_HEADER.length) == Secure.BEARER_HEADER) {
let token = request.get(Secure.HEADER_AUTH).substring(Secure.BEARER_HEADER.length);
var cert = fs.readFileSync(path.join(__dirname, "./cert/pub.pem")); // get public key
jwt.verify(token, cert, { algorithms: ["RS256"] }, function (err, decoded) {
request.user = decoded.user;
if (err) {
next(err);
} else {
next();
}
});
} else {
next();
}
}
- generate private key in PKCS#1 format
openssl genrsa -f4 -out private.txt 4096
- export public key
openssl rsa -in private.txt -outform PEM -pubout -out public.pem
- export private key to PKCS#8 format
openssl pkcs8 -topk8 -inform pem -in private.txt -outform PEM -nocrypt -out private.pem
There's multiple solution for this:
- If the method return a RestResult instance, the code from this instance and the data will return :
@Method.get("/:id")
@convert(Param)
testGet(value) {
return new RestResult(222, value);
}
- We can use the Method decorator that take a status in second parameter
@Method.get("/status/:id", 222)
@convert(Param)
testGetStatus(value) {
value.method = "get"
return value;
}
- The service class can implement a method named
manageStatus
. this method take the request objet and the service return value in parameter and must return the status
@Service.path("/three")
export default class ServiceManageStatus {
@Method.get("/:id")
@convert(Param)
testGet(value) {
value.method = "get"
return value;
}
manageStatus(request, valueToReturn) {
if(!valueToReturn && request.method.toUpperCase() == "GET") return 404;
return 222;
}
}
- Use the default static method
manageStatus
from Service class. You can overhide it if you need :- return value and post request return 201
- no return value and post request return 204
- no return value and get request return 404
- otherwise return 200
When you add a Hal decorator, the response of your service is compose like that :
{
"_links": {
"self": {
"href": "the link self"
}
},
"data": {
...
}
}
Data is the original response of your service.
If you want to add link to entity, you must decorate the entity class like that :
@Hal.halEntity("/authors/:id") // To set the path of the API, that's link id of the path with the id of the model
export default class Author {
@Hal.resourceId()
id = 0;
constructor(id, firstName, lastName, series) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.series = series;
}
}
The @Hal.halEntity decorator indicate to the framework the URI to find this resource. The ID is specified by putting a @Hal.resourceId() decorator on the right properties. So when you add a author in your response, Threerest will add a structure with link and data.
{
"_links": {
"self": {
"href": "/authors/3"
}
},
"data": {
"_links": {
"self": {
"href": "/authors/3"
}
},
"firstName": "Denis",
"id": "3",
"lastName": "Bajram",
"series": [
{
"_links": {
"self": {
"href": "/series/6"
}
},types
"idSerie": 6,
"name": "Expérience mort"
},
{
"_links": {
"self": {
"href": "/series/7"
}
},
"idSerie": 7,
"name": "Universal War One"
},
{
"_links": {
"self": {
"href": "/series/8"
}
},
"idSerie": 8,
"name": "Universal War Two"
}
]
}
}
If your service returns a given list, you can easily paginate the return by adding a boolean true to the decorator. @Pagination.paginate()
@Methods.get("/")
@Hal.halServiceMethod(true)
getAll() {
....
}
In this case, threerest react when tou add the parameter pageSize and/or pageIdx in your request. For example, the URI myAPI/authors send all the authors. If you want only the first 5 results, you just add have to send this request
myAPI/authors?pageSize=5
With the decorator, the pagination will be automatical executed. If you want to start to any other position, you must use pageIdx
myAPI/authors?pageSize=5&pageIdx=2
This URI send the first 5 results from the position 2 of the list.
If you want to use other terms that pageSize and pageIdx , you can specify them in the decorator.
@Methods.get("/")
@Hal.halServiceMethod({pageSize:"anotherlimit",pageIdx:"index"})
getAll() {
....
}
The URI becomes
myAPI/authors?anotherlimit=5&index=2