Server-Side Functionality with a Database

First Web Applications with Deno


Learning Objectives

  • You know what Deno is and can build a web application with Deno.
  • You know of different ways of using information from HTTP requests, including the path, parameters, and the method.

A web application with Deno

Deno is a runtime for running code. You can install it by following Deno installation guidelines.

Creating a simple web application with Deno is straightforward. The following code demonstrates a one-liner that could be used to create a server that responds to requests with the string “Hello world!”.

Deno.serve((request) => new Response("Hello world!"));

With Deno installed, you can run code using Deno’s run command from the command line. In your local file system, create a file called app.js and copy the above code to the file. Then, in the same folder in the terminal, run the following command.

deno run --allow-net --watch app.js

This runs the code, starting a server that responds to requests. When the server is running, open up the address http://localhost:8000 in the browser on your local computer.

We’ll continue with the walking skeleton when looking into Hono in the next chapter.

When you open up the address, the browser should show you the text “Hello world!”. If it does not, check that the server is running, that the address is correct, and that you do not have e.g. a firewall blocking the connection.

Starting and stopping the server

The command deno run --allow-net --watch app.js runs the code in the file app.js and starts the server. The flag --allow-net allows Deno to access the network, which is necessary for running a server. The flag --watch tells Deno to watch for changes in the file app.js and restart the server when the file changes.

To stop the server, press Ctrl + C on the keyboard (or, some other combination indicating cancel, depending on your operating system).

If you do not stop the server and try to run the command again, you will get an error message stating that the port is already in use.

Requests and responses

We can also explicitly write the one-liner out as follows.

const handleRequest = (request) => {
  return new Response("Hello world!");
};

Deno.serve(handleRequest);

Deno.serve takes an instance of a Request as a parameter and returns a Response. Request and Response are a part of the Web Fetch API, which is a standard for handling requests and responses in web applications.

Restructuring code

Our application can be further divided into two files, app.js and app-run.js. The app.js file contains the application code, and the app-run.js file is used to run the application. In this case, the file app.js would be as follows.

const handleRequest = (request) => {
  return new Response("Hello world!");
};

export default handleRequest;

And the app-run.js file would be as follows.

import handleRequest from "./app.js";

Deno.serve(handleRequest);

This is the format that is used in many of the exercises. Only modify the functionality in app.js, and when testing locally, run the server using the app-run.js file.

Loading Exercise...

On separating app.js and app-run.js

The files app.js and app-run.js are separated to make it easier to test the application code in app.js; this way, we do not need to run the server to test the functionality.

Accessing the server from the command line

Web applications can be queried from the command line using curl. It is by default installed on Linux and macOS, including WSL on Windows, and it can be installed on Windows.

There’s an extensive curl manual at https://everything.curl.dev/.

Open a separate terminal window from the one that you used to start the server, and run the command curl http://localhost:8000. The response from the server is shown in the terminal window.

$ curl http://localhost:8000
Hello world!
Loading Exercise...

In the examples in the materials, we use the dollar sign $ sign to indicate content that we write to the terminal. The dollar sign is not part of the command.

Modify the server code to match the following and save the file.

const handleRequest = (request) => {
  console.log("Hello server logs!");
  return new Response("Hello world!");
};

Deno.serve(handleRequest);

Now, when you run the curl command again, you should see that the server logs “Hello server logs!” to the terminal window where the server is running every.

The output is in the terminal window where the server is running, not in the terminal window where you made the request using curl.

Loading Exercise...

Information from requests

Requests in Deno follow the Web Fetch API Request interface specification. We can access request information through the instance properties of Request. As an example, to log the request method and the url, we can use the method and url properties of the request.

const handleRequest = (request) => {
  console.log(`Request method: ${request.method}`);
  console.log(`Request url: ${request.url}`);
  return new Response("Hello world!");
};

Deno.serve(handleRequest);

Requests can contain a variety of information, such as the request method, the requested URL, and request parameters.

Keep in mind that the requests and responses are done with HTTP. Deno and web application libraries and frameworks in general provide an abstraction for working with HTTP requests and responses.

Loading Exercise...

For easier handling of the url-property, which is a string, we can use the URL-class. URL provides convenience methods for working with URLs, allowing easy access to the requested path, parameters, and other parts of the URL.

You can pass a URL string to the constructor of the URL-class to create an instance of the class.

const handleRequest = (request) => {
  const url = new URL(request.url);
  // let's do something with the url

  return new Response("Hello world!");
};

Deno.serve(handleRequest);

Requested paths

The path of the URL, e.g. “/hello” from “http://localhost:8000/hello”, is stored in the property pathname of the URL object.

The following example outlines a server that responds with the message “world” to requests to path “/hello” and with the message “ingredient” to any path containing the string “secret”. Other requests receive the message “hello” as the response.

const handleRequest = (request) => {
  const url = new URL(request.url);

  let message = "hello";
  if (url.pathname === "/hello") {
    message = "world";
  } else if (url.pathname.includes("secret")) {
    message = "ingredient";
  }

  return new Response(message);
};

Deno.serve(handleRequest);
Loading Exercise...

Request parameters

Request parameters are added to the end of the URL after a question mark. As an example, the URL http://localhost:8000?name=Harry includes a parameter name with the value Harry.

Request parameters are stored in the searchParams property of the URL object. The searchParams property is an instance of the URLSearchParams class.

The following example outlines a server that responds with the value of the parameter name in the request.

const handleRequest = (request) => {
  const url = new URL(request.url);
  const params = url.searchParams;
  return new Response(`Name: ${params.get("name")}`);
};

Deno.serve(handleRequest);

When we try out the server, we see that the name is shown if it exists in the request parameters.

$ curl "http://localhost:8000?name=Harry"
Name: Harry%
$ curl "http://localhost:8000"
Name: null%

Parameters are separated by an ampersand ”&”. As an example, the URL http://localhost:8000?name=Harry&age=42 includes two parameters, “name” with the value “Harry” and “age” with the value “42”.

Loading Exercise...

As HTTP is a text-based protocol, request parameters and their values are always strings. If we wish to process request parameter values as numbers, we need to explicitly convert them to numbers. This can be done using the Number class or with the parseInt function.

The following application outlines how a simple calculator would be implemented. The application expects two parameters, “one” and “two”, and returns the sum of their values.

const handleRequest = (request) => {
  const url = new URL(request.url);
  const params = url.searchParams;

  const one = Number(params.get("one"));
  const two = Number(params.get("two"));

  return new Response(`Sum: ${one + two}`);
};

Deno.serve(handleRequest);
$ curl "http://localhost:8000?one=1&two=2"
Sum: 3
$ curl "http://localhost:8000?two=123"
Sum: 123
$ curl "http://localhost:8000?one=1&two=123"
Sum: 124
Template literals

In the above example, we used template literals. Template literals provide an easy way to construct strings with embedded expressions that are evaluated when the strings are constructed.

const one = 1;
const two = 2;
// logs "Sum: 3"
console.log(`Sum: ${one + two}`);
 // logs "Sum: 1 + 2 = 3"
console.log(`Sum: ${one} + ${two} = ${one + two}`);

Loading Exercise...

Request methods

Each HTTP request has a request method, such as GET or POST. The request method can be accessed through the property method of the request. The following application would respond with the request method.

const handleRequest = (request) => {
  return new Response(request.method);
};

Deno.serve(handleRequest);

By default, curl makes a GET request to the server.

$ curl http://localhost:8000
GET

In order to make another type of a request with curl, we need to use flag -X to pass the request method as a parameter. In the following example, we first make a request using the request method POST, after which we try to make a request with a request method called Alohomora.

$ curl -X POST http://localhost:8000
POST
$ curl -X Alohomora http://localhost:8000
Alohomora

Even though Alohomora is not a real request method, it still works. In practice, it is both the browser’s and the server’s responsibility to make sure to adhere to existing standards and protocols. One can, on the other hand, also invent new ones.

As an example, the HTTP status code 418 defined in RFC 2324 is “I’m a teapot”. The RFC (and the status code) was originally an April Fools joke, but the status code is still around.

Loading Exercise...

In the following example, the server responds with the message “Retrieving information, are you?” to GET requests, “Posting information, are you?” to POST requests, and with the message “Magicking, are you?” to requests made with Alohomora. If the request method is something else, the server responds with the message “I am unsure what I should do here!”.

const handleRequest = (request) => {
  let message = "I am unsure what I should do here!";
  if (request.method === "GET") {
    message = "Retrieving information, are you?";
  } else if (request.method === "POST") {
    message = "Posting information, are you?";
  } else if (request.method === "Alohomora") {
    message = "Magicking, are you?";
  }

  return new Response(message);
};

Deno.serve(handleRequest);

While Deno accepts unknown methods, you should only rely on valid HTTP methods such as GET, POST, PUT, DELETE, etc, in real-world applications.

Loading Exercise...

Summary

In summary:

  • Deno is a runtime for running JavaScript and TypeScript, and it can be used to create web servers with built-in HTTP support.
  • A simple web application can be created with Deno.serve, which takes a request and returns a response.
  • The application code can be separated into app.js for logic and app-run.js for starting the server, which makes testing easier.
  • Each HTTP request follows the Web Fetch API specification, and request information such as the path, parameters, and method can be accessed through the Request and URL interfaces.
  • A server can respond differently depending on the requested path, query parameters, or HTTP method.
  • Requests can be tested both in the browser and from the command line using tools such as curl.