# The Config File

As we previously discussed, the config file is a zum.toml file, writen using the TOML (opens new window) markup language. This file should contain two top level keys: the metadata key and the endpoints key.

# The metadata key

The metadata key contains everything that is needed in order to query the API, but is not data about an endpoint of the API itself. This key can hold the following keys:

  • server (required): The base URL where the API is hosted.

As an example, the first lines of your zum.toml file for a development environment should probably look similar to this:

[metadata]
server = "http://localhost:8000"

This indicates to zum that the API endpoints are located at http://localhost:8000. Easy enough, right?

# The endpoints key

The endpoints key contains every endpoint that you want to be able to interact with from zum. Each endpoint can hold the following keys:

  • route (required): The route to the endpoint itself.
  • method (required): The HTTP method (opens new window) to use when querying the endpoint.
  • params: The values to replace on the route with user-given values.
  • headers: The header keys to send with the request.
  • body: The body keys (and optionally types) to send with the request.

Notice that each endpoint is a sub-key within the endpoints top level key. This really shouldn't mean a lot to you as the developer, it just means that, when defining an endpoint, you should do it like this:

[endpoints.dada]
route = "/song"
method = "get"

Notice that the endpoint name (dada) follows a endpoints., which is the TOML convention for creating sub-keys within a certain key. If you defined the metadata key as the example on the previous section, to interact with that particular endpoint all you need to do is to run:

zum dada

With that, zum will send a GET HTTP request to http://localhost:8000/song 🎵. Just 5 lines on a TOML file!

You should now have a pretty reasonable understanding about exactly how the route and method keys work within each endpoint. Let's explore how the rest of the keys function!

# The params of an endpoint

This key is only required when you want to be able to interpolate a value on the route. For example, if you had a library of songs and wanted to GET /songs/{id}, you would probably want to pass the id value of the song through the CLI. To define this behaviour, let's define our TOML endpoint:

[endpoints.get-song]
route = "/songs/{id}"
method = "get"
params = ["id"]

There are a couple of interesting things in here. The first one is that there is now a bracket-surrounded chunk of the route string. This is the chunk that is intended to be replaced by a CLI input. The second interesting thing is that the params attribute is an array. This makes sense, as there might be more than one element to replace on the route, but we'll talk about this in a tiny bit. Finally, notice that the only element of the array matches what's inside the brackets of the route. This is very important, as it tells zum to replace that chunk of the route with what gets passed through the CLI. Speaking of the CLI, how do we use this endpoint?

zum get-song 57

Now, zum will send a GET HTTP request to http://localhost:8000/songs/57. Pretty cool!

Notice that you can place the brackets anywhere on the route string. This makes for an interesting posibility, as you can use it to query something inside an entity, for example. Imagine that you have an album entity with id 8 and you wanted to get all the songs inside that album with the string "saucy" on their names (and, of course, you have an endpoint that allows that). You could then define the endpoint as follows:

[endpoints.match-album-songs]
route = "/albums/{id}/songs?query={query}"
method = "get"
params = ["id", "query"]

Notice how we now have two parameters. To execute this query, all we need to do is to run:

zum 8 saucy

As you probably guessed, this will send a GET HTTP request to http://localhost:8000/albums/8/songs?query=saucy.

Here is when the array nature of the params key comes into play. So far, everything about this has been kind of natural. The first element to be replaced on the route was the id, the first element defined on the params array was the id and the first element that we passed to the CLI was the id. But for some people, it might be natural to call the CLI passing the query first, and then passing the id. Fixing this is easy. Just revert the order of the params on the params array!

[endpoints.match-album-songs]
route = "/albums/{id}/songs?query={query}"
method = "get"
params = ["query", "id"]

Now, you can call:

zum saucy 8

The order of the params array tells zum which CLI parameter to associate with each route replacement 🎉.

# The headers of an endpoint

The headers are defined exactly the same as the params. They are an array of strings, and their order modifies the order that is expected from the CLI. Let's see a small example to illustrate how to use them. Imagine that you have an API that requires JWT (opens new window) authorization to GET the songs of its catalog. Let's define that endpoint:

[endpoints.get-authorized-catalog]
route = "/catalog"
method = "get"
headers = ["Authorization"]

Now, to acquire the catalog, we would need to run:

zum get-authorized-catalog "Bearer super-secret-token"

⚠ Warning

Notice that, for the first time, we surrounded something with quotes on the CLI. The reason we did this is that, without the quotes, the console has no way of knowing if you want to pass a parameter with a space in the middle or if you want to pass multiple parameters, so it defaults to receiving the words as multiple parameters. To stop this from happening, you can surround the string in quotes, and now the whole string will be interpreted as only one parameter with the space in the middle of the string. This will be handy on future examples, so keep it in mind.

This will send a GET request to http://localhost:8000/catalog with the following headers:

{
    "Authorization": "Bearer super-secret-token"
}

And now you have your authorization-protected music catalog!

# The body of an endpoint

Just like params and headers, the body (the body of the request) gets defined as an array:

[endpoints.create-living-being]
route = "/living-beings"
method = "post"
body = ["name", "city"]

To run this endpoint, you just need to run:

zum create-living-being Dani Santiago

This will send a POST request to http://localhost:8000/living-beings with the following request body:

{
    "name": "Dani",
    "city": "Santiago"
}

As always, order matters.

[endpoints.create-living-being]
route = "/living-beings"
method = "post"
body = ["city", "name"]

Now, to get the same result as before, you should run:

zum create-living-being Santiago Dani

But what about types? On the examples above, all the request body parameters are being sent as strings. Of course, you can cast the values to some types. The supported types are:

  • string
  • integer
  • float
  • boolean (true or false accepted)
  • null (null accepted)

To declare a type, let's imagine you have an API endpoint that receives two numbers number1 and number2 and adds them together. To describe this endpoint, you can use the following description:

[endpoints.add]
route = "/add"
method = "post"
body = [
    { name = "number1", type = "integer" },
    { name = "number2", type = "integer" }
]

Notice how we are using a "new" dictionary notation, where the name key corresponds to the name of the body's key and the type key corresponds to the desired type. Now, when you run

zum add 5 8

The request body will be:

{
    "number1": 5,
    "number2": 8
}

Keep in mind that you can mix and match the definitions. You can even define the parameter with the dictionary notation and not include its type. You could define, for example:

[endpoints.example]
route = "/example"
method = "post"
body = [
    "parameter1",
    { name = "parameter2", type = "float" },
    { name = "parameter3" }
]

Now, parameter1 will be sent as a string, parameter2 will be casted as a float and parameter3 will also be sent as a string.

# Combining params, headers and body

Of course, sometimes you need to use some params, some headers and a body. For example, if you wanted to create a song inside an authorization-protected album (a nested entity), you would need to use the album's id as a param, the "Authorization" key inside the headers to get the authorization and the new song's data as the body. For this example, the song has a name (which is a string) and a duration in seconds (which is an integer). Let's describe this situation!

[endpoints.create-song]
route = "/albums/{id}/songs"
method = "post"
params = ["id"]
headers = ["Authorization"]
body = [
    "name",
    { name = "duration", type = "integer" }
]

Now, you can call the endpoint using:

zum create-song 8 "Bearer super-secret-token" "Con Altura" 161

This will call POST /albums/8/songs with the following headers:

{
    "Authorization": "Bearer super-secret-token"
}

And the following request body:

{
    "name": "Con Altura",
    "duration": 161
}

As you can probably tell, zum receives the params first on the CLI, then the headers and then the body. In pythonic terms, what zum does is that it kind of unpacks the three arrays consecutively, something like the following:

arguments = [*params, *headers, *body]
zum(arguments)