Two of the most common styles of web API design are RPC over JSON and REST. The choice between them is a matter of preference rather than functional and is akin to the choice between tabs and spaces. Luckily, with Microbus
you don’t have to choose: you can have both.
RPC is an abbreviation of remote procedure call and indeed this API style comes from a backend-centric perspective where each operation on the backend is a procedure. Endpoints on the backend are mapped to a URL path that reflects their name. Input arguments are passed as JSON in the body of the HTTP request or as query arguments. Output arguments are returned in the body of the HTTP response. In Microbus
, RPC is the default API style exactly because it is a consistent and unambiguous way to map the HTTP request to the underlying Go implementation of the endpoint.
Consider the following simple example:
functions:
- signature: Add(x int, y int) (sum int)
description: Add adds two integers and returns their sum.
Microbus
automatically generates marshaling and unmarshaling code that can process any of the following GET
or POST
requests:
In a GET
request, input arguments are read from the query arguments.
GET /add?x=5&y=6 HTTP/1.1
In a POST
request, the input arguments are read from the request body. Each of the arguments is expected to be a named field in a single JSON object.
POST /add HTTP/1.1
Content-Type: application/json
{"x":5,"y":6}
Alternatively, a URL-encoded form data can also be POST
-ed.
POST /add HTTP/1.1
Content-Type: application/x-www-form-urlencoded
x=5&y=6
In all three cases, the response is a JSON object where each of the return values (in this example only one) is a named field:
HTTP/1.1 200 OK
Content-Type: application/json
{"sum":11}
The REST API style comes from a web-centric perspective. Its philosophy is that everything on the web is a resource that can be identified with a URI and so a web API should reflect that. In REST, the request’s path represents the resource and the HTTP method (GET
, POST
, DELETE
, PUT
) represents the operation done on that resource. This style works best for CRUD APIs but may not translate well when the API has no object that is being worked on. For example, the Add
example from earlier doesn’t translate well to REST.
The following example defines a typical RESTful API for CRUD operations on an Object
. Notice how it utilizes the HTTP magic arguments httpRequestBody
, httpResponseBody
and httpStatusCode
.
functions:
- signature: Create(httpRequestBody *Object) (id int, httpStatusCode int)
description: Create creates an object.
method: POST
path: /objects
- signature: Read(id int) (httpResponseBody *Object)
description: Read reads an object.
method: GET
path: /objects/{id}
- signature: Update(id int, httpRequestBody *Object)
description: Update updates an existing object.
method: PUT
path: /objects/{id}
- signature: Delete(id int)
description: Delete deletes an existing object.
method: DELETE
path: /objects/{id}
- signature: ListAll(sortBy string, limit int, offset int) (httpResponseBody []*Object)
description: ListAll returns all objects.
method: GET
path: /objects
Create
expects a POST
request with the object to be created in the body of the request. It returns the assigned id
along with HTTP status code 201
.
POST /objects HTTP/1.1
Content-Type: application/json
{"foo":"bar","count":5,"etc":"..."}
HTTP/1.1 201 Created
Content-Type: application/json
{"id":1}
Read
expects a GET
request with the ID of the object in the path. It returns the matching object.
GET /objects/1 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{"foo":"bar","count":5,"etc":"..."}
Update
expects a PUT
request with the ID of the object in the path and the object to be created in the body of the request. It returns nothing.
PUT /objects/1 HTTP/1.1
Content-Type: application/json
{"foo":"bar","count":6,"etc":"..."}
HTTP/1.1 200 OK
Content-Type: application/json
{}
Delete
expects a DELETE
request with the ID of the object in the path. It returns nothing.
DELETE /objects/1 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
{}
ListAll
expects a GET
request with sort order and cursor information as query arguments. It returns a list of objects.
GET /objects?sortBy=count&limit=3&offset=0 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
[
{"foo":"bar","count":6,"etc":"..."},
{"foo":"baz","count":8,"etc":"..."},
{"foo":"bam","count":9,"etc":"..."}
]
Even if you have a preference as to one of these API styles, users of your API are likely to have a different preference. Luckily, implementing both styles is rather simple: create the RPC-styled endpoint first, then create the RESTful endpoint and call the RPC-styled endpoint under the hood.
Here’s how that will look like for the Read
endpoint discussed earlier:
functions:
- signature: Read(id int) (obj *Object)
description: Read an object by ID.
- signature: ReadREST(id int) (httpResponseBody *Object)
description: Read an object by ID.
method: GET
path: /objects/{id}
The implementation of the RPC-styled endpoint may then look something like this:
func (svc *Service) Read(ctx context.Context, id int) (obj *Object, err error) {
row, err := svc.sqlDatabase.QueryRowContext(ctx, "SELECT foo, count, etc FROM objects WHERE id=?", id)
if err != nil {
return nil, errors.Trace(err)
}
obj := &Object{}
err = row.Scan(&obj.Foo, &obj.Count, &obj.Etc)
if err != nil {
if errors.Is(sql.ErrNoRows) {
return Object{}, errors.Newcf(http.StatusNotFound, "object %v not found", id)
}
return nil, errors.Trace(err)
}
return obj, nil
}
The RESTful endpoint can then simply delegate the call to the above:
func (svc *Service) ReadREST(ctx context.Context, id int) (httpResponseBody *Object, err error) {
return svc.Read(ctx, id)
}