A beginner’s guide to building a static file server in Deno

Mayank C
Tech Tonic

--

In this article, we’ll step-by-step build a static file server in Deno. Static files are typically files such as HTML files, JavaScript files, CSS files, images, videos, etc. that aren’t server-generated, but must be sent to the browser when requested. Deno’s HTTP server does not serve any static files by default, you must configure it to serve the static content you want it to serve.

Let’s get started with the steps required in building a simple static file server.

Step 1

The first step is to write a simple hello world HTTP server using Deno’s standard library’s serve API.

import { serve } from "https://deno.land/std/http/mod.ts";const reqHandler = async (req: Request) => {
return new Response("Hello world!");
};
serve(reqHandler, { port: 8080 });

Let’s test it out using curl:

>> curl http://localhost:8080 -v
Hello world!

Step 2

The file path will come from the URL. For example — http://localhost:8080/app.html or http://localhost:8080/css/styles.css. The path to the file needs to be extracted from the URL. For now, we’ll send the extracted file path back in the HTTP response.

import { serve } from "https://deno.land/std/http/mod.ts";const reqHandler = async (req: Request) => {
const filePath = new URL(req.url).pathname;
return new Response(filePath);
};
serve(reqHandler, { port: 8080 });

Let’s test it out using curl:

>> curl http://localhost:8080/css/app.css
/css/app.css

>> curl http://localhost:8080/app.html
/app.html

The paths are getting extracted correctly.

Step 3

The file path will be relative to some directory that’s usually named public. Let’s update the code to use a directory called public for serving static resources.

import { serve } from "https://deno.land/std/http/mod.ts";const BASE_PATH = "./public";const reqHandler = async (req: Request) => {
const filePath = BASE_PATH + new URL(req.url).pathname;
return new Response(filePath);
};
serve(reqHandler, { port: 8080 });

Let’s test it out using curl:

> curl http://localhost:8080/app.html
./public/app.html

Step 4

The next two steps are combined into a single step.

First, we need to check for existence of the file.

Second, as Deno supports streaming of file, the file size needs to be determined in advance so that it can be set explicitly in the content-length header.

We’ll use a single call of Deno.stat to find the size of the file and catch the inexistent file error too. If the file is inexistent, a 404 response will be sent back.

import { serve } from "https://deno.land/std/http/mod.ts";const BASE_PATH = "./public";const reqHandler = async (req: Request) => {
const filePath = BASE_PATH + new URL(req.url).pathname;
let fileSize;
try {
fileSize = (await Deno.stat(filePath)).size;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response(null, { status: 404 });
}
return new Response(null, { status: 500 });
}

return new Response(filePath);
};
serve(reqHandler, { port: 8080 });

Let’s test it out using curl:

> ls -FR public
js/
public/js:
app.js smallApp.js
> curl http://localhost:8080/app.html
HTTP/1.1 404 Not Found
content-length: 0
> curl http://localhost:8080/js/app.js
HTTP/1.1 200 OK
content-length: 18
./public/js/app.js

Step 5

The validated file path will be opened using Deno.open which returns a Deno.FsFile object. The Deno.FsFile object contains a readable attribute that is a built-in stream (ReadableStream) to the opened file. This readable stream can be directly given to the body of the response object. Let’s update the code to serve the file instead of the file path.

import { serve } from "https://deno.land/std/http/mod.ts";const BASE_PATH = "./public";const reqHandler = async (req: Request) => {
const filePath = BASE_PATH + new URL(req.url).pathname;
let fileSize;
try {
fileSize = (await Deno.stat(filePath)).size;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response(null, { status: 404 });
}
return new Response(null, { status: 500 });
}

const body = (await Deno.open(filePath)).readable;
return new Response(body);
};
serve(reqHandler, { port: 8080 });

Let’s test it out using curl:

> curl http://localhost:8080/js/smallApp.js
HTTP/1.1 200 OK
console.log('Hello!');

The file is getting served. However, two important headers are still missing in the HTTP response. Let’s get them in.

Step 6

As Deno streams the file directly, it can’t determine the file size to set the content-length header. This header needs to be set explicitly.

import { serve } from "https://deno.land/std/http/mod.ts";const BASE_PATH = "./public";const reqHandler = async (req: Request) => {
const filePath = BASE_PATH + new URL(req.url).pathname;
let fileSize;
try {
fileSize = (await Deno.stat(filePath)).size;
} catch (e) {
if (e instanceof Deno.errors.NotFound) {
return new Response(null, { status: 404 });
}
return new Response(null, { status: 500 });
}

const body = (await Deno.open(filePath)).readable;
return new Response(body, {
headers: { "content-length": fileSize.toString() },
});
};
serve(reqHandler, { port: 8080 });

Let’s test it out using curl:

> > ls -l public/js/
total 576
-rw-r--r-- 1 mayankc staff 288580 Apr 6 21:23 app.js
-rw-r--r-- 1 mayankc staff 23 Apr 6 21:30 smallApp.js
> curl http://localhost:8080/js/smallApp.js
HTTP/1.1 200 OK
content-length: 23
console.log('Hello!');

Step 7

The final step is to set the content-type header. There is a need to maintain a mapping of possible extensions to content types. The easiest to convert a file path to content-type is by using the third party media_types module. This module provides a simple API lookup that takes the file path as input and returns the content-type header value.

The final code is as follows:

Let’s test it out using curl:

> curl http://localhost:8080/someApp.html
HTTP/1.1 404 Not Found
> curl http://localhost:8080/js/someOtherApp.js
HTTP/1.1 404 Not Found
> curl http://localhost:8080/app.html
HTTP/1.1 200 OK
content-length: 44
content-type: text/html

<HTML>
<BODY>
Hello World!!
</BODY>
</HTML>
> curl http://localhost:8080/js/app.js -o /dev/null
HTTP/1.1 200 OK
content-length: 288580
content-type: application/javascript
<< .. OUTPUT SUPPRESSED .. >>

The simple static server works fine!

--

--