REST API / NATS Gateway
This section describes how we can use the easer
server as a REST API Proxy that delegates the incoming REST API calls to microservices, that are implemented in a separate, standalone application.
We will use the following example projects for demonstration:
- The person-rest-api is the specification of REST API endpoints of a simple person service;
- The person-service-js is the JavaScript implementation of REST API endpoints of a simple person service.
First clone these repositories:
git clone git@github.com:tombenke/person-rest-api.git
git clone git@github.com:tombenke/person-service-js.git
The easer
REST API Proxy loads the endpoint descriptors from OAS format files. After loading the descriptors, it immediately is able to respond to the incoming REST calls.
Start the easer, with the REST API:
$ easer -r person-rest-api/rest-api/api.yml
2019-08-05T04:03:03.117Z [easer@4.0.0] info: Load endpoints from /home/tombenke/topics/easer-tutorial/person-rest-api/rest-api/api.yml
2019-08-05T04:03:03.187Z [easer@4.0.0] info: Start up webServer
2019-08-05T04:03:03.200Z [easer@4.0.0] info: Express server listening on port 3007
2019-08-05T04:03:03.201Z [easer@4.0.0] info: App runs the jobs...
Now the server is running and the REST endpoints are available, so try to call them:
$ curl http://localhost:3007/persons -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3007 (#0)
> GET /persons HTTP/1.1
> Host: localhost:3007
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 501 Not Implemented
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 78
< ETag: W/"4e-mVxPm4tUvCVz9epJEB6Bbkm4UMA"
< Date: Mon, 05 Aug 2019 04:03:21 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
{"error":"The endpoint is either not implemented or `operationId` is ignored"}
We got 501
error in the response, and the body says that the endpoint is not implemented.
This is because the easer
is responsible for the REST API at the edge, and not for the implementation of them, so the responses will be errors in all cases.
In order to bind the incoming endpoint calls with the service functions, the easer
uses a messaging middleware, and synchronous, topic based, RPC-like calls to the service functions. The requests are passed towards the service implementations in JSON format messages through a previously agreed topic.
The service implementations are independently running, standalone applications.
The following figure shows the architecture of the REST API / NATS Gateway Mode
in order to have a completely working system, we need to start all the three main components:
- The NATS middleware,
- The application, that implements the service endpoints,
- The easer, which is configured to connect to the NATS server, and to forward the traffic towards the service implementations.
First, let's start the NATS server, using Docker:
$ docker run -it --rm --network=host -p 4222:4222 -p 6222:6222 -p 8222:8222 --name nats-main nats
[1] 2019/08/05 04:21:42.975522 [INF] Starting nats-server version 2.0.2
[1] 2019/08/05 04:21:42.975579 [INF] Git commit [6a40503]
[1] 2019/08/05 04:21:42.975687 [INF] Starting http monitor on 0.0.0.0:8222
[1] 2019/08/05 04:21:42.975760 [INF] Listening for client connections on 0.0.0.0:4222
[1] 2019/08/05 04:21:42.975782 [INF] Server id is NBW6XLRZ4SJRIRYTRDLVHUTBY55DLH64T6IDP37MRW2MMA3E7DIPLI6P
[1] 2019/08/05 04:21:42.975791 [INF] Server is ready
[1] 2019/08/05 04:21:42.976324 [INF] Listening for route connections on 0.0.0.0:6222
The NATS server will provide its services on the nats://localhost:4222
URI.
Then starts the service application, in a new terminal:
$ node person-service-js/index.js
Finally restart the easer server with the following parameters:
$ easer -r person-rest-api/rest-api/api.yml -n nats://localhost:4222 --topicPrefix person-demo -u
2019-08-05T04:43:04.362Z [easer@4.0.0] info: Start up nats
2019-08-05T04:43:04.384Z [easer@4.0.0] info: Load endpoints from /home/tombenke/topics/easer-tutorial/person-rest-api/rest-api/api.yml
2019-08-05T04:43:04.449Z [easer@4.0.0] info: Start up webServer
2019-08-05T04:43:04.461Z [easer@4.0.0] info: Express server listening on port 3007
2019-08-05T04:43:04.462Z [easer@4.0.0] info: App runs the jobs...
Where:
-n nats://localhost:4222
: defines the URI of the NATS server;--topicPrefix person-demo
: defines the prefix for the name of the topic to use. The topic name is generated by the following pattern:<topicPrefix.<endpoint.method>_<endpoint.uri>
;-u
: Enableseaser
to forward the incoming calls as messages to the NATS topic.
If we try to call again the REST API, it will successfully serve the request:
$ curl http://localhost:3007/persons -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 3007 (#0)
> GET /persons HTTP/1.1
> Host: localhost:3007
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 2
< ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
< Date: Mon, 05 Aug 2019 04:47:38 GMT
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
[]
The response is an empty array, since there is no persons
uploaded to the services yet.
We can use other endpoints to make sure, the API is working correctly:
$ curl -X PUT http://localhost:3007/persons/leia -H "Content-type: application/json" -d '{"id":"leia","familyName":"Organa","givenName":"Leia"}'
{"id":"leia","familyName":"Organa","givenName":"Leia"}
$ curl http://localhost:3007/persons
[{"id":"leia","familyName":"Organa","givenName":"Leia"}]
The messages that the easer
sends to the NATS topic contain the most relevant parts of the incoming request, such as: the query and path parameters, the headers, the method, the path, and the body. Beside the request, this message also contains the minimized version of the service descriptor itself, to help the service implementation to successfully do its job.
The RPC-like topic call works on a way, that the messaging system creates a temporary response topic, that the service implementation can use for sending the response back to the easer
server.
So the service endpoint will send back another message, that has to contain all the information which is needed to create a well formed REST response, such as: the status code, the headers, and the body, if there is any.
This is an example for an incoming GET /persons
request that is sent to the get_/persons
topic:
{
"topic": "easer.put_/persons/{personId}",
"method": "put",
"uri": "/persons/{personId}",
"endpointDesc": {
"uri": "/persons/{personId}",
"jsfUri": "/persons/:personId",
"method": "put",
"operationId": null,
"consumes": [],
"produces": [
"application/json"
],
"responses": {
"200": {
"status": "200",
"headers": {},
"examples": {
"application/json": {
"noname": {
"mimeType": "application/json",
"value": {
"id": "2a1152ee-4d77-4ff4-a811-598555937625",
"familyName": "Skywalker",
"givenName": "Luke"
}
}
}
}
},
"400": {
"status": "400",
"headers": {},
"examples": {
"application/json": {}
}
}
}
},
"request": {
"cookies": {},
"headers": {
"host": "localhost:3007",
"user-agent": "curl/7.81.0",
"accept": "*/*",
"content-type": "application/json",
"content-length": "67"
},
"parameters": {
"query": {},
"uri": {
"personId": "luke-skywalker"
}
},
"body": "{\"id\":\"luke-skywalker\",\"familyName\":\"Skywalker\",\"givenName\":\"Luke\"}"
}
}
The topic name is generated via the following pattern from the endpoint definition: <topicPrefix>.<method>_<uri>
,
that is easer.get_/persons
in this specific case.
At the NATS message level the easer also sends some headers. These are the followings:
{
"content-type":"application/json",
"message-type":"rpc/request"
}
It informs the responders about the representation of the message (the content-type
), and the type of the payload (message-type
).
The service implementations are independent applications, that can run in a distributed environment. For the same endpoint, there may be several implementations exist at the same time, so how will the messages find the right service implementation?
So the easer
passes the incoming requests towards these services.
The service handlers sends back the responses to easer
that finally forwards the responses towards the client.
In this case the getPersonsServiceHandler()
service handler function will respond with two things:
the NATS message headers, and the payload in stringified form, as the examples show below:
The response JSON object (which will be sent to the gateway in stringified format):
{
"status": 200,
"headers": {
"Content-type": "application/json"
},
"body": []
}
And the payload (the stringified version of the JSON response object):
"{\"status\":200,\"headers\":{\"Content-type\":\"application/json\"},\"body\":{\"id\":\"luke-skywalker\",\"familyName\":\"Skywalker\",\"givenName\":\"Luke\"}}"
The response headers in the NATS message:
{
"content-type":"application/json",
"message-type":"rpc/response"
}