Echo is an exercise created by my friend @soulim to experiment, learn and find different ways of building REST APIs with Ruby.
The main purpose of Echo is to serve ephemeral/mock endpoints created with parameters specified by clients
In this iteration I'm trying to find the simplest way of building a REST API that it's easy to maintain. In order to accomplish that these are the decisions I made
- Stick to Rack apps, no Rails.
- Follow the API design first approach:
- The OpenAPI will be the only source of true for the API, any change to the REST API will start with an update on the specification.
- The commitee middlewares will validate that the requests meet the OpenAPI specifications, coerce the request parameters and provide spec helpers to assert that the code fulfill the OpenAPI contract
- The REST API will follow JSON:API specification.
- The API specification development will be done with Stoplight. There is no need to edit an OpenAPI specification manually.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
- Echo or server: it's what I'm going to implement :)
- mock endpoint: the main purpose of Echo is to serve ephemeral/mock endpoints created with parameters specified by clients
- Endpoints API: a set of endpoints (
GET|POST|PATCH|DELETE /endpoints{/:id}
) designated to manage mock endpoints served by Echo. See examples in "Technical specifications" section
- The server MUST implement
GET /endpoints
endpoint. The server MUST return a list of mock endpoints created by you, usingPOST /endpoints
endpoint, or an empty array if no mock endpoints are defined yet. See examples in "Technical specifications" section. - The server MUST implement
POST /endpoints
endpoint. The server creates a mock endpoint according to data from the payload. The server SHOULD validate received data. See examples in "Technical specifications" section. - The server MUST implement
PATCH /endpoints{/:id}
endpoint. The server updates the existing mock endpoint according to data from the payload. The server SHOULD NOT accept invalid data or update non-existing mock endpoints. If requested mock endpoint doesn't exist, the server MUST respond with404 Not found
. See examples in "Technical specifications" section. - The server MUST implement
DELETE /endpoints{/:id}
endpoint. The server deletes the requested mock endpoint. If requested mock endpoint doesn't exist, the server MUST respond with404 Not found
. See examples in "Technical specifications" section. - The server MUST serve mock endpoints as they defined by clients. Mock endpoints MUST be available over HTTP. All mock endpoints are available as they defined. Example: if there is a mock endpoint
POST /foo/bar/baz
, it MUST be available only forPOST
requests at/foo/bar/baz
path. It SHALL NOT be available viaGET /foo/bar/baz
or evenPOST /foo/bar
because these are different endpoints. Basically Echo works like "what you define is what you get". - Validate incoming requests as might contain invalid data.
- The server MAY implement authentication for Endpoints API.
The server operates on Endpoint entities:
Endpoint {
id String
verb String
path String
response {
code Integer
headers Hash<String, String>
body String
}
}
id
(required), a string value that uniquely identifies an Endpointverb
(required), a string value that may take one of HTTP method names. See RFC 7231path
(required), a string value of the path part of URLresponse
(required), an object with following attributes:code
(required), an integer status code returned by Endpointheaders
(optional), a key-value structure where keys represent HTTP header names and values hold actual values of these headers returned by Endpointbody
(optional), a string representation of response body returned by Endpoint
Echo serves mock endpoints as they previously defined by the clients via Endpoints API. For example it will serve { "message": "Hello world" }
as response from GET /foo/bar
endpoint, if this endpoint was defined upfront via Endpoints API.
List endpoints
#### RequestGET /endpoints HTTP/1.1
Accept: application/vnd.api+json
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": [
{
"type": "endpoints",
"id": "12345",
"attributes": [
"verb": "GET",
"path": "/greeting",
"response": {
"code": 200,
"headers": {},
"body": "\"{ \"message\": \"Hello, world\" }\""
}
]
}
]
}
Create endpoint
#### RequestPOST /endpoints HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "endpoints",
"attributes": {
"verb": "GET",
"path": "/greeting",
"response": {
"code": 200,
"headers": {},
"body": "\"{ \"message\": \"Hello, world\" }\""
}
}
}
}
HTTP/1.1 201 Created
Location: http://example.com/greeting
Content-Type: application/vnd.api+json
{
"data": {
"type": "endpoints",
"id": "12345",
"attributes": {
"verb": "GET",
"path": "/greeting",
"response": {
"code": 200,
"headers": {},
"body": "\"{ \"message\": \"Hello, world\" }\""
}
}
}
}
Update endpoint
#### RequestPATCH /endpoints/12345 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "endpoints",
"id": "12345"
"attributes": {
"verb": "POST",
"path": "/greeting",
"response": {
"code": 201,
"headers": {},
"body": "\"{ \"message\": \"Hello, everyone\" }\""
}
}
}
}
HTTP/1.1 200 OK
Content-Type: application/vnd.api+json
{
"data": {
"type": "endpoints",
"id": "12345",
"attributes": {
"verb": "POST",
"path": "/greeting",
"response": {
"code": 201,
"headers": {},
"body": "\"{ \"message\": \"Hello, everyone\" }\""
}
}
}
}
Delete endpoint
#### RequestDELETE /endpoints/12345 HTTP/1.1
Accept: application/vnd.api+json
HTTP/1.1 204 No Content
Error response
In case client makes unexpected response or server encountered an internal problem, Echo should provide proper error response.DELETE /endpoints/1234567890 HTTP/1.1
Accept: application/vnd.api+json
HTTP/1.1 404 Not found
Content-Type: application/vnd.api+json
{
"errors": [
{
"code": "not_found",
"detail": "Requested Endpoint with ID `1234567890` does not exist"
}
]
}
Sample scenario
#### 1. Client requests non-existing path> GET /hello HTTP/1.1
> Accept: application/vnd.api+json
HTTP/1.1 404 Not found
Content-Type: application/vnd.api+json
{
"errors": [
{
"code": "not_found",
"detail": "Requested page `/hello` does not exist"
}
]
}
> POST /endpoints HTTP/1.1
> Content-Type: application/vnd.api+json
> Accept: application/vnd.api+json
>
> {
> "data": {
> "type": "endpoints",
> "attributes": {
> "verb": "GET",
> "path": "/hello",
> "response": {
> "code": 200,
> "headers": {
> "Content-Type": "application/json"
> },
> "body": "\"{ \"message\": \"Hello, world\" }\""
> }
> }
> }
> }
HTTP/1.1 201 Created
Location: http://example.com/hello
Content-Type: application/vnd.api+json
{
"data": {
"type": "endpoints",
"id": "12345",
"attributes": {
"verb": "GET",
"path": "/hello",
"response": {
"code": 200,
"headers": {
"Content-Type": "application/json"
},
"body": "\"{ \"message\": \"Hello, world\" }\""
}
}
}
}
> GET /hello HTTP/1.1
> Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{ "message": "Hello, world" }
The server responds with HTTP 404 because only GET /hello
endpoint is defined.
NOTE: if you could imagine different behavior from the server, feel free to propose it in your solution.
> POST /hello HTTP/1.1
> Accept: application/vnd.api+json
HTTP/1.1 404 Not found
Content-Type: application/vnd.api+json
{
"errors": [
{
"code": "not_found",
"detail": "Requested page `/hello` does not exist"
}
]
}