A Hello World Example

The client API or browser sends a HTTP request to the ArangoDB server and the server returns a HTTP response to the client. A HTTP request consists of a method, normally GET or POST when using a browser, and a request path like /hello/world. For a real Web server there are a zillion of other thing to consider, we will ignore this for the moment. The HTTP response contains a content type, describing how to interpret the returned data, and the data itself.

In the following example, we want to define an action in ArangoDB, so that the server returns the HTML document

<html>
  <body>
   Hello World
  </body>
</html>

if asked GET /hello/world.

The server needs to know what function to call or what document to deliver if it receives a request. This is called routing. All the routing information of ArangoDB is stored in a collection _routing. Each entry in this collections describes how to deal with a particular request path.

For the above example, add the following document to the _routing collection:

arangosh> db._routing.save({ 
........>  url: { 
........>    match: "/hello/world" 
........>  },
........>  content: { 
........>    contentType: "text/html", 
........>    body: "<html><body>Hello World</body></html>" 
........>  }
........> });
show execution results

In order to activate the new routing, you must either restart the server or call the internal reload function.

arangosh> require("internal").reloadRouting()

Now use the browser and access http:// localhost:8529/hello/world

You should see the Hello World in our browser:

shell> curl --dump - http://localhost:8529/hello/world

HTTP/1.1 200 OK
content-type: text/html

"Hello World"

Matching a URL

There are a lot of options for the url attribute. If you define different routing for the same path, then the following simple rule is applied in order to determine which match wins: If there are two matches, then the more specific wins. I. e, if there is a wildcard match and an exact match, the exact match is preferred. If there is a short and a long match, the longer match wins.

Exact Match

If the definition is

{ 
  url: { 
    match: "/hello/world" 
  } 
}

then the match must be exact. Only the request for /hello/world will match, everything else, e. g. /hello/world/my or /hello/world2, will not match.

The following definition is a short-cut for an exact match.

{ 
  url: "/hello/world" 
}

Note: While the two definitions will result in the same URL matching, there is a subtle difference between them:

The former definition (defining url as an object with a match attribute) will result in the URL being accessible via all supported HTTP methods (e.g. GET, POST, PUT, DELETE, ...), whereas the latter definition (providing a string url attribute) will result in the URL being accessible via HTTP GET and HTTP HEAD only, with all other HTTP methods being disabled. Calling a URL with an unsupported or disabled HTTP method will result in an HTTP 501 (not implemented) error.

Prefix Match

If the definition is

{ 
  url: { 
    match: "/hello/world/*" 
  } 
}

then the match can be a prefix match. The requests for /hello/world, /hello/world/my, and /hello/world/how/are/you will all match. However /hello/world2 does not match. Prefix matches within a URL part, i. e. /hello/world*, are not allowed. The wildcard must occur at the end, i. e.

/hello/*/world 

is also disallowed.

If you define two routes

{ url: { match: "/hello/world/*" } }
{ url: { match: "/hello/world/emil" } }

then the second route will be used for /hello/world/emil because it is more specific.

Parameterized Match

A parameterized match is similar to a prefix match, but the parameters are also allowed inside the URL path.

If the definition is

{ 
  url: { 
    match: "/hello/:name/world" 
  } 
}

then the URL must have three parts, the first part being hello and the third part world. For example, /hello/emil/world will match, while /hello/emil/meyer/world will not.

Constraint Match

A constraint match is similar to a parameterized match, but the parameters can carry constraints.

If the definition is

{ 
  url: { 
    match: "/hello/:name/world", 
    constraint: { 
      name: "/[a-z]+/" 
    } 
  }
}

then the URL must have three parts, the first part being hello and the third part world. The second part must be all lowercase.

It is possible to use more then one constraint for the same URL part.

{ 
  url: { 
    match: "/hello/:name|:id/world",
    constraint: { 
      name: "/[a-z]+/", id: "/[0-9]+/" 
    } 
  }
}

Optional Match

An optional match is similar to a parameterized match, but the last parameter is optional.

If the definition is

{ 
  url: { 
    match: "/hello/:name?", 
    constraint: { 
      name: "/[a-z]+/"
    } 
  }
}

then the URL /hello and /hello/emil will match.

If the definitions are

{ url: { match: "/hello/world" } }
{ url: { match: "/hello/:name", constraint: { name: "/[a-z]+/" } } }
{ url: { match: "/hello/*" } }

then the URL /hello/world will be matched by the first route, because it is the most specific. The URL /hello/you will be matched by the second route, because it is more specific than the prefix match.

Method Restriction

You can restrict the match to specific HTTP methods.

If the definition is

{ 
  url: { 
    match: "/hello/world", 
    methods: [ "post", "put" ] 
  }
}

then only HTTP POST and PUT requests will match. Calling with a different HTTP method will result in an HTTP 501 error.

Please note that if url is defined as a simple string, then only the HTTP methods GET and HEAD will be allowed, an all other methods will be disabled:

{ 
  url: "/hello/world" 
}

More on Matching

Remember that the more specific match wins.

  • A match without parameter or wildcard is more specific than a match with parameters or wildcard.
  • A match with parameter is more specific than a match with a wildcard.
  • If there is more than one parameter, specificity is applied from left to right.

Consider the following definitions

arangosh> db._routing.save({ 
........>  url: { match: "/hello/world" },
........> content: { contentType: "text/plain", body: "Match No 1"} });
arangosh> db._routing.save({ 
........>  url: { match: "/hello/:name", constraint: { name: "/[a-z]+/" } },
........> content: { contentType: "text/plain", body: "Match No 2"} });
arangosh> db._routing.save({ 
........>  url: { match: "/:something/world" },
........> content: { contentType: "text/plain", body: "Match No 3"} });
arangosh> db._routing.save({ 
........>  url: { match: "/hi/*" },
........> content: { contentType: "text/plain", body: "Match No 4"} });
arangosh> require("internal").reloadRouting()
show execution results

Then

shell> curl --dump - http://localhost:8529/hello/world

HTTP/1.1 200 OK
content-type: text/plain

"Match No 1"
shell> curl --dump - http://localhost:8529/hello/emil

HTTP/1.1 200 OK
content-type: text/plain

"Match No 2"
shell> curl --dump - http://localhost:8529/your/world

HTTP/1.1 200 OK
content-type: text/plain

"Match No 3"
shell> curl --dump - http://localhost:8529/hi/you

HTTP/1.1 200 OK
content-type: text/plain

"Match No 4"

You can write the following document into the _routing collection to test the above examples.

{
  routes: [
{ url: { match: "/hello/world" }, content: "route 1" },
{ url: { match: "/hello/:name|:id", constraint: { name: "/[a-z]+/", id: "/[0-9]+/" } }, content: "route 2" },
{ url: { match: "/:something/world" }, content: "route 3" },
{ url: { match: "/hello/*" }, content: "route 4" },
  ]
}