- TypeScript 97.7%
- HTML 1.8%
- Dockerfile 0.5%
| .zed | ||
| handlers | ||
| tests | ||
| utils | ||
| .gitignore | ||
| deno.json | ||
| deno.lock | ||
| DEVELOPMENT.md | ||
| Dockerfile | ||
| README.md | ||
| server.ts | ||
| serverus.ts | ||
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 lineSERVERUS_HANDLERS: a list of ;-separated directories to look for handlers inSERVERUS_PUT_PATHS_ALLOWED: a list of ;-separated directories for which file uploads via PUT are allowedSERVERUS_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
Responseshort-circuits the rest of the chain and that response is sent. - Steps are just functions -- any function returning
Response | undefinedcan be either the terminal handler or an auth check, depending on what it does. - The array form
[ ...prechecks, handler ]lets you compose: if anyprecheckreturns a response (like a 401), control never reacheshandler.
_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