Testing an API with emacs and restclient

When we're developing some application we frequently interact with APIs.

There are applications like postman, httpie, insomnia and so on to accomplish this task but having an external application only to test a few endpoints or even a complex API is a little overkill.

Using emacs and a great package called restclient.el we can have a very complete tool to handle API requests without leaving our favorite editor.

Installation

Put these lines of code in your emacs configuration and you'll be ready to go.


  (use-package restclient
    :ensure t
    :mode (("\\.http\\'" . restclient-mode)))

Here we're using use-package to install restclient.el and also we're configuring restclient to use extension .http to enable its features.

Now if we open a file with .http extension restclient will be enabled automatically.

Example api

We'll be using a example API to test the features of restclient so I prepared a little API in flask with a few endpoints to allow us to check the features of restclient.

This is the code of the application in case you are curious


  from uuid import uuid4

  from flask import Flask, escape, jsonify, make_response, request

  app = Flask(__name__)

  items = [{"uid": uuid4().hex, "name": f"item {i + 1}"} for i in range(3)]


  SECRET = "password"


  @app.route("/")
  def index():
      name = request.args.get("name", "World")
      return f"Hello, {escape(name)}!"


  @app.route("/api")
  def api():
      return jsonify({"version": 1.0})


  @app.route("/api/items")
  def list_items():
      return jsonify({"data": items})


  @app.route("/api/items", methods=["post"])
  def create_item():
      if authenticated(request):
          new_item = {"uid": uuid4().hex, "name": request.json.get("name")}
          items.append(new_item)
          return make_response(jsonify({"data": new_item}), 201)
      else:
          return make_response(jsonify({"error": "please provide credentiales"}), 401)


  def authenticated(req):
      token = req.headers.get("authorization")
      return token is not None and token == SECRET

If you don't want to install any other software to test this API you can use a docker image that contains this application.

Just run the following command and you'll have an API running on port 5000.


  docker run -p 5000:5000 erickgnavar/restclient-api-example:0.1

Now we're ready to test out API using restclient.

Usage

Let's see some examples of how can we use restclient from within emacs but first lets create a file called api.http and open it from emacs.

Make a GET request

We can execute this code using C-c C-c to show the results in the current buffer or use C-c C-v to show them in a new buffer.


  GET http://localhost:5000/?name=guest
  Content-Type: application/json
Hello, guest!
<!-- GET http://localhost:5000/?name=guest -->
<!-- HTTP/1.0 200 OK -->
<!-- Content-Type: text/html; charset=utf-8 -->
<!-- Content-Length: 13 -->
<!-- Server: Werkzeug/0.16.0 Python/3.6.9 -->
<!-- Date: Tue, 29 Oct 2019 05:34:44 GMT -->
<!-- Request duration: 0.023261s -->

As we can see we can define a http request in plain text. We just need to define the method and the URL of our API. In this case we're querying the root of the API. Also the response is presented in plain text including some useful data like http headers and the request duration. We can define the http request headers as well.

In this case the response use an html format as we can see in the Content-Type response header.

Note: when you create a file with many requests in it make sure you split them using a comment #, for example:


  GET http://localhost:5000/?name=foo
  # a split
  GET http://localhost:5000/?name=bar
  # a split
  GET http://localhost:5000/?name=baz

If you don't add a separator an error will be raised when you try to execute the request.

JSON responses

Now lets try to fetch a json type endpoint. Restclient identifies the content-type of the response and use an emacs mode that fits with the content-type. In this case the response is a json object so restclient enable js-mode to present the response.


  GET http://localhost:5000/api
  Content-Type: application/json
{
  "version": 1.0
}
// GET http://localhost:5000/api
// HTTP/1.0 200 OK
// Content-Type: application/json
// Content-Length: 16
// Server: Werkzeug/0.16.0 Python/3.6.9
// Date: Tue, 29 Oct 2019 05:42:01 GMT
// Request duration: 0.025286s

Let's try with another endpoint that has more interesting information.


  GET http://localhost:5000/api/items
  Content-Type: application/json
{
  "data": [
    {
      "name": "item 1",
      "uid": "931d90b493e944d9816061f46b57ce92"
    },
    {
      "name": "item 2",
      "uid": "edf9c8dda1ed4e8da205c53d9978ede2"
    },
    {
      "name": "item 3",
      "uid": "57a5146e3c98479785374f38e9e4c056"
    }
  ]
}
// GET http://localhost:5000/api/items
// HTTP/1.0 200 OK
// Content-Type: application/json
// Content-Length: 188
// Server: Werkzeug/0.16.0 Python/3.6.9
// Date: Tue, 29 Oct 2019 05:42:33 GMT
// Request duration: 0.026217s

Variables and dynamic content

What happen if we need to pass some extra information to make an http request? In restclient we can have variables and we use them in the definition of the request. In this case we'll define a password variable which contains the required Authorization value to be able access to this endpoint. Also we can define the payload of the request, in this case a json object.

First let's try a wrong password to see what happen.


  :password = wrong-password

  POST http://localhost:5000/api/items
  Content-Type: application/json
  Authorization: :password
  {
      "name": "new item"
  }
{
  "error": "please provide credentiales"
}
// POST http://localhost:5000/api/items
// HTTP/1.0 401 UNAUTHORIZED
// Content-Type: application/json
// Content-Length: 40
// Server: Werkzeug/0.16.0 Python/3.6.9
// Date: Tue, 29 Oct 2019 05:47:24 GMT
// Request duration: 0.036553s

We received a 401 response because the credentiales we used are not correct. Now let's try it again but now with the correct credentials.


  :password = password

  POST http://localhost:5000/api/items
  Content-Type: application/json
  Authorization: :password
  {
      "name": "new item"
  }
{
  "data": {
    "name": "new item",
    "uid": "f1ede16e39754b3eb735627e78d26146"
  }
}
// POST http://localhost:5000/api/items
// HTTP/1.0 201 CREATED
// Content-Type: application/json
// Content-Length: 70
// Server: Werkzeug/0.16.0 Python/3.6.9
// Date: Tue, 29 Oct 2019 05:48:15 GMT
// Request duration: 0.034962s

As we can see the request was made successfully. Variables in restclient are evaluated at the time the request is made so we can define a variable and use it in as many requests as we want. This is useful when we're working with APIs that need some authentication to allow us to access to their endpoints. We can request a token then save it and use it for the rest of the request we've defined in our file.

Other useful features

Convert request to curl format

If we need to pass a request with its data to some friend who doesn't use emacs we can pass the request definition(it's just plain text after all) but we can also generate a curl command so it's going to be easy for anyone to test the request.

We can use C-c C-u from within out request to generate a curl command. After we execute this keybinding the curl command will be copied to the clipboard.

If we use this in our previous example we'll get the following curl command:


  curl -i -H Authorization\:\ password -H Content-Type\:\ application/json -XPOST http\://localhost\:5000/api/items -d \{'
  '\ \ \ \ \"name\"\:\ \"new\ item\"'
  '\}

Now we can paste this in a terminal and the request will be made.

Navigate through the available requests

From the same author we have restclient-helm this package allow us to jump easily to a specific request using the combination C-c C-g. This is useful if we are working with an extensive API and we want to find some request quickly.

This package use helm to present the available options and when we chose one the cursor will jump to the selection.

Formatting payload

If we are using json as the request body we'll need to have it formatted in some way. We can use json-mode for accomplish this.

Now our installation code will be:


  (use-package json-mode
    :ensure t)

  (use-package restclient
    :ensure t
    :defer t
    :mode (("\\.http\\'" . restclient-mode))
    :bind (:map restclient-mode-map
                ("C-c C-f" . json-mode-beautify)))

We're adding a new keybinding to restclient-mode-map so we can use C-c C-f to format the request body.

Conclusion

Having our requests defined in plain text allow us to use it even as documentation and we don't depend of some external app that use a custom format to store these requests. We can freely pass this .http file to anyone and they will be able to read it and understanding it without the need to install an application.