Work with HTTP in Deno
Interacting with HTTP servers in Deno is easy due to Deno’s adherence to web
standards. For example, Deno uses standards like
Request
,
Response
or
Fetch
. The latter is largely
implemented according to the WHATWG fetch
spec, but
either deviates in some places or is missing implementations
(Spec Deviations of fetch
).
Deno is also part of the WinterCG, a community group
that aims to develop standardised APIs between JavaScript runtimes. Part of the
minimum common API are the
Request
and Response
standards mentioned above. In the following chapters we
will use some of these web standard classes. These make working with HTTP much
easier, as we do not have to redefine many of the things we need.
Web API Standards
Request & Response
Request
is an object that we can either use to make fetch
requests, or to
get information about a request on the server side. This makes working with
APIs, for example, more convenient and standardised. We get a lot of information
about the request in an object. The definition below is an example:
const req = new Request("https://example.com", {
headers: { "content-type": "application/json" },
});
console.log(req.url); // https://example.com
console.log(req.method); // GET
console.log(req.headers); // Headers { "content-type": "application/json" }
Here we set up a request that we can use for a fetch
request, for example by
setting the URL to https://example.com
and the content type we expect to JSON
(application/json
). Unless otherwise specified, the request method is GET
by
default. We will see the same application on the server side in the following
chapters. The same handling also applies to the Response
object.
We will see how to process and read a Request
object in our HTTP server in the
chapter on the Deno.serve
HTTP server.
URL & URLPattern
const url = new URL("https://example.com/my-path?query=string");
console.log(url.hostname); // example.com
console.log(url.protocol); // https:
console.log(url.pathname); // /my-path
console.log(url.search); // ?query=string
console.log(url.searchParams.get("query")); // string
Using URLPattern
we can easily match other URL strings or parts of them
against a given pattern. This allows us to create API routes for our HTTP
server. Even dynamic values are possible. More on this later.
const SINGLE_BOOK = new URLPattern({ pathname: "/book/" });
const url = new URL("https://example.com/book");
// check if the URL matches the request
if (SINGLE_BOOK.exec(url)) {
console.log("The path matches");
}
As you can see here, we define the URLPattern
with the pathname /book/
.
Accordingly, the if
will only resolve to true
if the URL is exactly
/book/
. If, for example, the /
is missing at the end or other things are
appended to this path, e.g. /book/test-book
, the if
will not be executed.
Now let’s get started and create an HTTP server and use the APIs we discussed. We will see how well Deno can be used without a framework to create servers and APIs.
Deno.serve HTTP Server
Basic Server
Creating HTTP servers is one of the easiest things to do in Deno. Using
Deno.serve
and a simple Response
object, we can provide a basic HTTP server.
To do this we create a file with the following code. For example main.ts
.
const handler = (_req: Request): Response => {
return new Response("Hello, World!");
};
Deno.serve(handler);
To start the server, we need to remember that we need to give network
permissions. So we have to start the server with deno run --allow-net main.ts
.
If we want to test our server, we can use curl to get a response in another
terminal window.
➜ curl http://localhost:8000
Hello, World!
We have now created the simplest type of HTTP server in Deno. Now let’s look at how to implement routes for our HTTP server.
Listen to Paths
Currently, our HTTP server would respond to every request with Hello, World!
.
This makes limited sense. So let’s look at how we can define different paths,
including dynamic parameters.
As mentioned earlier, we have a number of web standards in Deno. One of them is
URLPattern
. We can
use this to add different paths to our HTTP server without using any other
frameworks.
const ALL_BOOKS = new URLPattern({ pathname: "/books/" });
This definition prepares us to compare the incoming request with our URL pattern and respond with a specific response.
URLPattern implements an
exec
method
that we can use to match another URL. Let’s have a look at an example.
const ALL_BOOKS_PATTERN = new URLPattern({ pathname: "/books/" });
This allows us to create a /books/
route. In other words, we can easily check
if the incoming request is against this path.
If we extend our HTTP server to check for this route, we get the following `handler’.
const ALL_BOOKS_PATTERN = new URLPattern({ pathname: "/books/" });
const handler = (req: Request): Response => {
const url = new URL(req.url);
if (ALL_BOOKS_PATTERN.exec(url.pathname)) {
return new Response("All books");
}
return new Response("Hello, World!");
};
Deno.serve(handler);
Set Headers and handle those
HTTP servers must also set headers so that the client can interpret the data returned correctly. Since we are working with the `Response’ object, we can add these to the object. The headers will then be sent to the client in the response.
Deno.serve((_req) =>
new Response("Hello World", {
status: 200,
headers: { "my-cool-header": "Look!" },
})
);
Here we set the my-cool-header
for every response that goes against our HTTP
server. Now if we start the server with deno run --allow-net http.ts
, we can
make a request against our server. For example, with curl
curl -i http://localhost:8000
we get the following response:
➜ curl -i http://localhost:8000
HTTP/1.1 200 OK
my-cool-header: Look!
content-type: text/plain;charset=UTF-8
vary: Accept-Encoding
content-length: 11
date: Fri, 20 Dec 2024 16:49:11 GMT
Hello World
As we can see, the header my-cool-header: Look!
header is now set. This means
that we can configure the content-type
or charset
etc. manually as well.
HTTP/1.1 and HTTP/2 support
Support for HTTP versions 1.1 and 2 works out of the box. We do not need to do anything to take advantage of HTTP/2 (e.g. request multiplexing or header compression). Read more about the advantages HTTP/2 vs. HTTP/1.1:.
Automatic Body Compression
Another out-of-the-box feature that Deno builds into the HTTP server is the automatic compression of response bodies (gzip or brotli, depending on what the client supports). In order for this to work automatically, we need to consider 3 things.
- The
Request
has anAccept-Encoding
header with eithergzip
orbr
(brotli) set. - The response object must also have a
Content-Type
header set, which is compressible. Deno uses the mime-db/db database to inform it of compressible headers. The headers built into Deno can be found in deno/ext/http/compressible.rs. - The response body must be greater than 64 bytes.
Read more: Writing an HTTP Server on Deno.com
Basic CRUD Server (Get, Post, Patch, Delete)
We have covered all the basics to create a simple CRUD (Create, Read, Update, Delete) server. In terms of content, we want to have an HTTP server in which we manage books.
We will start with the two books ‘Atomic Habits’ and ‘The Alchemist’. This list will be expandable. So we need routes to show us 1. all books and 2. a single book by ID. We want to be able to type in a title and create a new book in our list. We also want to be able to change and delete a book title using the ID.
The code for this is as follows
// Define the routes necessary
const ALL_BOOKS = new URLPattern({ pathname: "/books/" });
const SINGLE_BOOK = new URLPattern({ pathname: "/books/:id" });
// Define our "database" of books
const books: Record<string, string> = {
"1": "Atomic Habits",
"2": "The Alchemist",
};
// GET /books/
const getBooks = (_req: Request): Response => {
return new Response(JSON.stringify(books), {
status: 200,
});
};
// GET /books/:id
const getBook = (req: Request): Response => {
const url = new URL(req.url);
const id = SINGLE_BOOK.exec(url)?.pathname.groups?.id;
if (!id) {
return new Response("Invalid id", { status: 400 });
}
const book = books[id];
if (!book) {
return new Response("Book not found", { status: 400 });
}
return new Response(book, {
status: 200,
});
};
// POST /books/
const postBook = async (req: Request): Promise<Response> => {
const { title } = await req.json();
if (!title) {
return new Response("Invalid title", { status: 400 });
}
const id = Object.keys(books).length + 1;
books[id] = title;
return new Response("Book added", { status: 201 });
};
// PATCH /books/:id
const patchBook = async (req: Request): Promise<Response> => {
const { title } = await req.json();
const url = new URL(req.url);
const id = SINGLE_BOOK.exec(url)?.pathname.groups?.id;
if (!id) {
return new Response("Invalid id", { status: 400 });
}
books[id] = title;
return new Response(`Book updated: ${id}: ${title} `, { status: 200 });
};
// DELETE /books/:id
const deleteBook = (req: Request): Response => {
const url = new URL(req.url);
const id = SINGLE_BOOK.exec(url)?.pathname.groups?.id;
if (!id) {
return new Response("Invalid id", { status: 400 });
}
delete books[id];
return new Response(`Book ${id} deleted`, { status: 200 });
};
const handler = async (req: Request): Promise<Response> => {
// Check if /books/ is requested
if (ALL_BOOKS.exec(req.url)) {
switch (req.method) {
case "GET":
return getBooks(req);
case "POST":
return await postBook(req);
default:
return new Response("Method not allowed", { status: 405 });
}
}
// Check if /books/:id is requested
if (SINGLE_BOOK.exec(req.url)) {
switch (req.method) {
case "GET":
return getBook(req);
case "PATCH":
return patchBook(req);
case "DELETE":
return deleteBook(req);
default:
return new Response("Method not allowed", { status: 405 });
}
}
// if none of the above paths are requested
return new Response("Not found", { status: 404 });
};
Deno.serve(handler);
In Deno, a simple CRUD server without any framework, using only web standard APIs, is created from these 110 lines.
Rewriting our HTTP Server with Hono
While it is possible to build an HTTP server without a framework, there are point in favor of using one.
One of them is a simpler API and therefore more readable code. Readable code => maintainable code
There are also ready-made modules for CORS, authentication or logging. These features can of course be built by us, but they are more time consuming.
Let’s have a look at the framework Hono. Like Deno, it is based on web standards and therefore supports any other JavaScript runtime (e.g. Bun, Cloudflare Workers, NodeJS) in addition to Deno. Hono also offers the middleware features mentioned above (e.g. CORS support or basic auth). In the following example, we will look at a translation of the previous example.
We will see that Hono’s API makes it easier to define our routes. We can omit
URLPattern
and switch
statements and still get the routes we want, including
dynamic ID parameters.
import { Context, Hono } from "jsr:@hono/hono";
const app = new Hono();
const books: Record<string, string> = {
"1": "Atomic Habits",
"2": "The Alchemist",
};
// GET /books/
const getBooks = (c: Context) => {
return c.json(books);
};
// GET /books/:id
const getBook = (c: Context): Response => {
const id = c.req.param("id");
if (!id) {
return c.text("Invalid id", { status: 400 });
}
const book = books[id];
if (!book) {
return c.text("Book not found", { status: 400 });
}
return c.text(book);
};
// POST /books/:id
const postBook = async (c: Context): Promise<Response> => {
const { title } = await c.req.json();
if (!title) {
return c.text("Invalid title", { status: 400 });
}
const id = Object.keys(books).length + 1;
books[id] = title;
return c.text("Book added", { status: 201 });
};
// PATCH /books/:id
const patchBook = async (c: Context): Promise<Response> => {
const { title } = await c.req.json();
const id = c.req.param("id");
if (!id) {
return c.text("Invalid id", { status: 400 });
}
books[id] = title;
return c.text(`Book updated: ${id}: ${title} `, { status: 200 });
};
// DELETE /books/:id
const deleteBook = (c: Context): Response => {
const id = c.req.param("id");
if (!id) {
return c.text("Invalid id", { status: 400 });
}
delete books[id];
return c.text(`Book ${id} deleted`, { status: 200 });
};
app
.get("/books/", getBooks)
.post("/books/", postBook)
.get("/books/:id", getBook)
.patch("/books/:id", patchBook)
.delete("/books/:id", deleteBook);
Deno.serve(app.fetch);
Concluding Deno and HTTP
Throughout this exploration of HTTP servers in Deno, we’ve seen how the runtime’s commitment to web standards and modern design principles makes it an excellent choice for building web applications. The key takeaways include:
-
Web Standards First: Deno’s implementation of web standards like
Request
,Response
, andURLPattern
makes it intuitive for developers familiar with web development, reducing the learning curve and maintaining consistency with browser APIs. -
Simplicity Without Sacrifice: The native HTTP server implementation (
Deno.serve
) provides robust functionality out of the box, including:- HTTP/1.1 and HTTP/2 support
- Automatic body compression
- Efficient request handling
-
Framework Optional: While frameworks like Hono can provide additional convenience and features, Deno’s standard library is powerful enough to build fully functional CRUD applications with minimal boilerplate code.
-
Performance: Thanks to its Rust-based implementation using the Hyper library, Deno’s HTTP server provides excellent performance characteristics without requiring developers to manage low-level details.
The comparison between the vanilla Deno implementation and the Hono framework version demonstrates that while frameworks can provide more structured and concise APIs, Deno’s native capabilities are more than sufficient for many use cases. This flexibility allows developers to choose the right level of abstraction for their specific needs, whether that’s using Deno’s built-in features or incorporating additional frameworks and libraries.