Routes and Request Methods
Learning objectives
- Knows what a router, a route, and a controller are.
- Can create routes and handle requests made to different paths with different methods.
- Knows how to use path variables.
When building web applications with vanilla Deno, we used the term path to identify the path (part of an address or URI, e.g. /tasks/1
) to which the request had been made. The path was where the request was made was, in part, used to determine the code that should be executed for the specific request.
When working with oak, we can extract the path (or pathname) from the url within the request. This information could be used to direct the requests to correct functionality. As such behavior is often needed, however, oak provides routing middleware that helps mapping paths to functions.
The term router refers to a part of the framework that is responsible for determining which function should be called for which path and which request method. A router directs (or routes) requests to correct functions. A route, then, can be seen as a mapping between a path and a request method to a function that is responsible for handling the specific request.
Creating and using a router
Routes are created using the Router
class from oak. In the following example, we import both the application and the Router
class from oak. We create an instance of the application and an instance of the router. Then, we define a function called greet
that uses the response object from the context to set the response to 'Hello world!'.
After this, we add the function greet
to the router. When calling router.get('/', greet)
, we state that whenever a GET
request is made to the path /
, the request should be routed to the function greet
. This is followed by a call app.use(router.routes())
, where we ask the application to use the routes (or route) that we just defined.
Finally, we ask the application to start listening for requests.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const router = new Router();
const greet = ({ response }) => {
response.body = "Hello world!";
};
router.get("/", greet);
app.use(router.routes());
app.listen({ port: 7777 });
When we run the application and send a request to it, we see the expected response.
curl http://localhost:7777
Hello world!%
Now, when we access other paths than the one that we have previously defined for the router, we see a response that indicates that the page was not found. Indeed, if we look into the response in curl, the server returns a response with the status code 404 when we request a path that has not been added to the router.
curl http://localhost:7777/test
curl -v http://localhost:7777/test
// ..
< HTTP/1.1 404 Not Found
< content-length: 0
// ..
It is meaningful to notice that the application didn't actually crash and that it provided a response that tells us that the page was not found, even though we did not explicitly tell the application to do so. Previously, when using vanilla Deno, we had to explicitly respond with the status code 404 if we wished that the server would produce such a response. Magic!
Question not found or loading of the question is still in progress.
Multiple routes
Adding multiple routes is straightforward. For every route that we wish that the application handles, we create a mapping from a path to a function using the router. In the example below, we have defined two paths that the application listens to. When a request is made to the root path of the application, the response will contain the string 'Hello world!', while when a request is made to the path '/another', the application response will contain the string 'Another path!'.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const router = new Router();
const greet = ({ response }) => {
response.body = "Hello world!";
};
const another = ({ response }) => {
response.body = "Another path";
};
router.get("/", greet);
router.get("/another", another);
app.use(router.routes());
app.listen({ port: 7777 });
When we start the application, requests to the paths added to the router produce the responses that we expect.
curl http://localhost:7777
Hello world!%
curl http://localhost:7777/another
Another path!%
Request methods
We previously mentioned that the router is used to map paths and methods to functions. So far, we have used the get
-method of the router; this leads to mapping HTTP GET requests. The router also has other methods that correspond to the remaining HTTP request methods. The example below shows the use of get
, post
, and delete
methods of the router, which correspond to the HTTP methods with the same names.
In the example below, we named the functions that handle the requests get
, post
, and del
-- we use del
instead of delete
, as delete
is a reserved word in JavaScript.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const router = new Router();
const get = ({ response }) => {
response.body = "HTTP GET";
};
const post = ({ response }) => {
response.body = "HTTP POST";
};
const del = ({ response }) => {
response.body = "HTTP DELETE";
};
router.get("/", get);
router.post("/", post);
router.delete("/", del);
app.use(router.routes());
app.listen({ port: 7777 });
When we launch the server, the server responds appropriately to the requests made using the HTTP GET, HTTP POST, and HTTP DELETE request methods. When using other request methods or paths that have not mapped with the router, we receive the status code 404 as a response.
curl http://localhost:7777
HTTP GET%
curl -X POST http://localhost:7777
HTTP POST%
curl -X DELETE http://localhost:7777
HTTP DELETE%
curl -v -X PUT http://localhost:7777
// ...
< HTTP/1.1 404 Not Found
< content-length: 0
// ...
curl -v -X GET http://localhost:7777/path
// ...
< HTTP/1.1 404 Not Found
< content-length: 0
// ...
Path variables
When working with vanilla Deno, we used the split method to retrieve parts of paths that corresponded to, e.g., a database id of an object. While the approach worked, it is somewhat cumbersome and does not generalize too well.
Web frameworks typically come with a way to define path variables, i.e. using parts of the path as a variable. The values of these variables can be passed as parameters to the function that is handling requests made to a particular path.
Defining a path variable is done using a colon, i.e. :
, which is followed by a name for that variable. The following example shows how a path variable is defined for a route.
router.get('/names/:id', getName);
The above example would route all requests made to the path /names/:id
to the function getName
, where :id
can be almost any value (excluding e.g. /
-marks). Parameters are added to a variable called params
that is in context
object. The following example shows how the params object within the context object are used. The example both logs the parameter value for the path variable id
and returns a response that tells the user that such a parameter was used.
const getName = ({ params, response }) => {
console.log("Parameters were as follows:");
console.log(params);
console.log(`The value of the id extracted from path is: ${params.id}`);
response.body = `The path variable id had the value ${params.id}`;
};
Combined together, an application using the routing and the method shown above could look as follows.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const router = new Router();
const getName = ({ params, response }) => {
console.log("-- a new request --");
console.log("Parameters were as follows:");
console.log(params);
console.log(`The value of the id extracted from path is: ${params.id}`);
response.body = `The path variable id had the value ${params.id}`;
};
router.get("/names/:id", getName);
app.use(router.routes());
app.listen({ port: 7777 });
Now, when we launch the server, making GET requests to the path /names/:id
shows us a response that contains the value of the path variable.
curl http://localhost:7777/names/123
The path variable id had the value 123%
curl http://localhost:7777/names/hello
The path variable id had the value hello%
When the above requests are made to the server, the server logs the following messages. As we can see, params
is a JavaScript object.
-- a new request --
Parameters were as follows:
{ id: "123" }
The value of the id extracted from path is: 123
-- a new request --
Parameters were as follows:
{ id: "hello" }
The value of the id extracted from path is: hello
A route can also include multiple path variables. In the following example, we define a path that has three path variables. The first path variable corresponds to a year, the second path variable corresponds to a month, and the third path variable corresponds to a day. The application can be used to check whether the given day exists.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const router = new Router();
const isInCalendar = ({ params, response }) => {
const date = new Date(
Number(params.year),
Number(params.month) - 1,
Number(params.day),
);
console.log(date);
if (
date.getFullYear() == params.year &&
date.getMonth() + 1 == params.month &&
date.getDate() == params.day
) {
response.body = "The day is in the calendar!";
} else {
response.body = "The day is not in the calendar!";
}
};
router.get("/year/:year/month/:month/day/:day", isInCalendar);
app.use(router.routes());
app.listen({ port: 7777 });
curl http://localhost:7777/year/2020/month/11/day/31
The day is not in the calendar!%
curl http://localhost:7777/year/2030/month/12/day/12
The day is in the calendar!%
curl http://localhost:7777/year/2030/month/0/day/12
The day is not in the calendar!%
Request parameters
Request parameters, that is, parameters added to the url after a question mark, are parsed into a variable called searchParams
that is placed into the url
-object of the request. The searchParams
object is an instance of URLSearchParams.
The following application demonstrates the use of request parameters. If there is a request parameter called id
, the application returns the value of id
. Otherwise, the application returns the string "Not defined".
import { Application } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const requestParamId = ({ request, response }) => {
if (request.url.searchParams.has("id")) {
response.body = request.url.searchParams.get("id");
} else {
response.body = "Not defined";
}
};
app.use(requestParamId);
app.listen({ port: 7777 });
Now, when we launch the application and make a few requests to it, we see that the server parses the request parameters for our disposal.
curl http://localhost:7777
Not defined%
curl "http://localhost:7777?id=7"
7%
curl "http://localhost:7777?id=42"
42%
Chaining routes
Note that as the get
, post
, etc methods of the router return the router itself, it is possible to add routes using method chaining. While we previously added each route in a separate call, as shown below.
router.get('/', get);
router.post('/', post);
router.delete('/', del);
Using method chaining, we can add the routes to the router also as follows.
router.get("/", get)
.post("/", post)
.delete("/", del);
Both approaches are valid.
Naming of router functionality
Router functionality in our projects is typically placed into files in a subfolder called controllers
of a folder called routes
. Later on, we will add another folder called apis
to the routes
-folder, but for now simply the folder controllers
suffices.
In practice, the term controller refers to an entity that how requests made to the server should be handled.
Using the new folder structure, the following application would be divided as follows.
import { Application, Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
const app = new Application();
const router = new Router();
const get = ({ response }) => {
response.body = "HTTP GET";
};
const post = ({ response }) => {
response.body = "HTTP POST";
};
const del = ({ response }) => {
response.body = "HTTP DELETE";
};
router.get("/", get)
.post("/", post)
.delete("/", del);
app.use(router.routes());
app.listen({ port: 7777 });
We would create a folder called routes
and within that a folder called controllers
. In the folder controllers
, we would have a files that export functionality used for handling individual requests (like the functions get
, post
, and del
above. Then, in the folder routes
, we would have a file called routes.js
that would import the files from the folder controllers
, and map the functionality from the controllers to paths and methods.
Let's call the file with the above functions methodController.js
(which is not really a good name, but oh well..).
When looking at the project as a tree, it looks as follows.
tree --dirsfirst
.
├── routes
│ ├── controllers
│ │ └── methodController.js
│ └── routes.js
└── app.js
2 directories, 3 files
The file methodController.js
defines functionality used for handling requests. When built from the above example, it would contain the three functions that it would then export.
const get = ({ response }) => {
response.body = "HTTP GET";
};
const post = ({ response }) => {
response.body = "HTTP POST";
};
const del = ({ response }) => {
response.body = "HTTP DELETE";
};
export { del, get, post };
The file routes.js
then imports the functionality defined in the controllers, and creates a Router
object that is then used to map the functionality from the controllers to paths and methods. Finally, the file routes.js
exports the router object, which can be then used in the main application file.
import { Router } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import * as helloController from "./controllers/methodController.js";
const router = new Router();
router.get("/", helloController.get)
.post("/", helloController.post)
.delete("/", helloController.del);
export { router };
Now, the main application file, here app.js
, imports the Application from oak and the router from the routes.js
file that is in the folder routes
. Then, it creates an application, adds the routes to it, and starts to listen to requests.
import { Application } from "https://deno.land/x/oak@v12.6.1/mod.ts";
import { router } from "./routes/routes.js";
const app = new Application();
app.use(router.routes());
app.listen({ port: 7777 });
Looks a bit cleaner than the file that we started with.
Front controller
While we used the term controller above to depict an entity that is responsible for deciding what to do for a request, there exists also a concept called front controller. A front controller is a specific type of a controller that analyzes each request made to the server, and decides what should be done with each request. In oak, the Router
in practice implements front controller functionality -- every request will go through the router (which is a middleware!), which then, based on the mappings that we have given, decides which function the specific request should be forwarded to.