No description
  • TypeScript 97.7%
  • HTML 1.8%
  • Dockerfile 0.5%
Find a file Use this template
2026-07-06 16:31:49 -07:00
.zed feature: support .spa files for Single Page Apps 2025-11-20 16:44:49 -08:00
handlers refactor: better handling for HTTP method handler exports from 2026-07-06 16:05:49 -07:00
tests fix: test some more potential pitfalls 2026-07-06 16:31:49 -07:00
utils feature: allow for randomized includes 2026-06-24 07:31:50 -07:00
.gitignore feature: allow for static file uploads and deletions 2025-08-01 20:11:17 -07:00
deno.json fix: test some more potential pitfalls 2026-07-06 16:31:49 -07:00
deno.lock refactor: logging rework 2026-04-13 18:30:33 -07:00
DEVELOPMENT.md refactor: remove PRECHECKS, instead just make every route a 2026-07-05 16:46:43 -07:00
Dockerfile fix: let serverus run as root inside the container so it can read 2025-07-14 20:05:47 -07:00
README.md docs: update readme, again 2026-07-06 16:10:35 -07:00
server.ts refactor: PRECHECKS are gone, now static and spa handlers export 2026-07-05 20:39:17 -07:00
serverus.ts refactor: change options and defaults handling 2026-04-28 17:56:18 -07:00

SERVERUS

A flexible HTTP server for mixed content. Throw static files, markdown, Typescript and (hopefully, eventually) more into a directory and serverus can serve it up a bit more like old-school CGI.

Usage

You just start serverus in a directory (or specify a root) and it starts listening and tells you its address.

Execution

Compiled:

[user@machine] ~/ serverus --root ./public

Container:

podman run -d -p 8000:8000 -v /var/public:/www --name web andyburke/serverus:latest

Deno:

deno --allow-env --allow-read --allow-write --allow-net jsr:@andyburke/serverus --root ./public

Overview

SERVERUS is a Deno-based webserver that allows for various handlers to serve up different types of content with a great deal of control.

The default handlers are:

HTML with SSI support

<html>
	<body>
		<!-- #include "./header.html" -->

		<!-- can include markdown, which will be converted to html -->
		<!-- #include "./essay.md" -->

			<!-- you can include a random file from a glob pattern, eg: -->
		<!-- #include random './essay-*.md' -->

			<!-- you can include a random file from a list, eg: -->
		<!-- #include random './essay-01.md' or './essay-03.md' or './essay-05.html' -->

			<div id="footer">
			<!-- you can include text files as well -->
			<!-- #include "./somedir/footer.txt" -->

			<!-- you can chain includes to allow for local overrides or temporary notices -->
			<!-- #include "./news.html" or "./default.html" -->
		</div>
	</body>
</html>

Markdown

Serverus will serve markdown as HTML (or raw with an Accept header of text/markdown set).

Typescript

Serverus tries to flexibly serve up Tyepscript you've got under the root. It tries to be smart about dispatching requests based on parameterized folder names.

If your modules export HTTP method handers like GET and POST, they will be called for those requests.

You can also create files like:

www/
  api/
    book/
      :book_id/
        delete.ts <- case-insensitive HTTP method matches this file for DELETE
        index.ts <- can have anything method handlers not defined in their own files here
        GET.ts
        POST.ts

Static files

Serverus serves up static files within the root folder as one would expect for a simpler web server.

Environment Variables

  • SERVERUS_ROOT: set the root, aka --root on the command line
  • SERVERUS_HANDLERS: a list of ;-separated directories to look for handlers in
  • SERVERUS_PUT_PATHS_ALLOWED: a list of ;-separated directories for which file uploads via PUT are allowed
  • SERVERUS_DELETE_PATHS_ALLOWED: a list of ;-separated directories for which file deletions via DELETE are allowed

Singe-Page Applications (SPA)

If you place a .spa file under the root, that directory will try to return an index.html or index.htm file that lives in it for any requests that are relative to it. For example:

www/
  app/
    .spa
    index.html
    static/
      .spa.static
      some_static_file.txt

If you have this file structure (assuming www is the root), a GET to /app/foo/bar will return the index.html file in the app/ directory. (Which would then presumably handle the url in window.location appropriately.)

If you have a subdirectory you still want to allow 404s to happen in, you can place a .spa.static file in your tree to skip the typical SPA handler.

Typescript Handling

These types define the structure of a serverus TypeScript route handler. A route is represented as one value per HTTP method; each value runs in order, and if any returns a Response, that response is sent immediately and subsequent steps are skipped.

You can write either form:

  • A single function for simple routes — the dispatcher treats it as a one-element chain.
  • An array of functions when you want pre-flight checks to run before the terminal handler.
import type { METHOD_HANDLER } from './serverus/handlers/typescript.ts';

/** One step in a route chain. Returning `undefined` (or nothing) lets the next step run. */
export type METHOD_HANDLER = (
	request: Request,
	meta: Record<string, any>,
	request_info: Deno.ServeHandlerInfo
) => Response | undefined | Promise<Response | undefined>;

/** A chain of steps — or a single function. The dispatcher normalises both forms to an array. */
export type ROUTE_HANDLER_CHAIN = readonly METHOD_HANDLER[] | METHOD_HANDLER;

interface ROUTE_HANDLER {
	GET?: ROUTE_HANDLER_CHAIN;
	POST?: ROUTE_HANDLER_CHAIN;
	PUT?: ROUTE_HANDLER_CHAIN;
	DELETE?: ROUTE_HANDLER_CHAIN;
	PATCH?: ROUTE_HANDLER_CHAIN;
	default?: ROUTE_HANDLER_CHAIN;   // used when no method-specific chain is provided
}

The meta object includes params (URL parameters), query, and cookies. A step that returns nothing runs the next element in the array.

A route is matched by walking files under SERVERUS_ROOT; a .ts file at routes/api/users/:user_id/index.ts becomes the handler for /api/users/:user_id/. The loader picks the per-method chain first and falls back to default if absent.

Example: authentication + terminal step
// routes/admin/dashboard/index.ts
import type { METHOD_HANDLER } from '@serverus/handlers/typescript.ts';

function require_admin( request ): Response | undefined {
	if ( !request.headers.get( 'x-api-key' ) ) {
		return new Response( 'Unauthorized', { status: 401 } );
	}
}

function show_dashboard( _request, meta: Record<string, any> ): Response {
	const user_id = meta.params.user_id;
	return Response.json( { message: `Welcome admin ${user_id}` } );
}

export const GET: readonly METHOD_HANDLER[] = [ require_admin, show_dashboard ];

require_admin runs first; it short-circuits by returning a 401 response when the header is missing. Otherwise control passes to show_dashboard, which always returns a final response.

Sharing steps across methods

Steps are just plain functions — there's no framework-specific wiring needed:

const log_it: METHOD_HANDLER = ( request, _meta ) => {
	console.log( `${request.method} ${new URL( request.url ).pathname}` );
};

export const GET: readonly METHOD_HANDLER[] = [ log_it, get_handler ];
export const POST: readonly METHOD_HANDLER[] = [ log_it, post_handler ];

No aggregator file is needed. Each method just references the steps it wants in order.

Splitting a handler across multiple files

When one route grows too large for a single index.ts, split the HTTP-method handlers into their own .ts files and have index.ts compose them with shared pre-checks:

routes/admin/dashboard/
  index.ts          # composes GET chain with auth check + terminal step
  get_handler.ts    # body of GET (returns list, no auth logic)
  post_handler.ts   # body of POST (creates resource)
  delete_handler.ts # body of DELETE (removes resource)
// routes/admin/dashboard/index.ts
import type { METHOD_HANDLER } from '@serverus/handlers/typescript.ts';
import { shared_precheck } from './auth_check.ts';
import { get_handler } from './get_handler.ts';
import { post_handler } from './post_handler.ts';
import { delete_handler } from './delete_handler.ts';

/** share one pre-check across every method — it runs before the terminal step for that method. */
export {
	GET: [ shared_precheck, get_handler ],
	POST: [ shared_precheck, post_handler ],
	DELETE: [ shared_precheck, delete_handler ]
}
// routes/admin/dashboard/get_handler.ts -- one small file per method
import type { METHOD_HANDLER } from '@serverus/handlers/typescript.ts';

/** the body of GET. no auth logic -- that lives in index.ts above as a shared pre-check step. */
export const get_handler: METHOD_HANDLER = ( _request, meta ) => {
	const user_id: string = meta.params.user_id;
	return Response.json( { users: fetch_users_for_user( user_id ) } );
};

How the chain model works.

  • A step that returns a Response short-circuits the rest of the chain and that response is sent.
  • Steps are just functions -- any function returning Response | undefined can be either the terminal handler or an auth check, depending on what it does.
  • The array form [ ...prechecks, handler ] lets you compose: if any precheck returns a response (like a 401), control never reaches handler.

_pre.ts files

Any _pre.ts files found under the root that export .load() and/or .unload() methods will be loaded and those functions will be called at server startup/shutdown, respectively.

NOTE ON WINDOWS

Because Windows has more restrictions on filenames, you can use ___ in place of : in parameter directories.

TODO

  • reload typescript if it is modified on disk
  • wrap markdown converted to html in a div with a class for styling