James Stanley


The web program manifesto

Tue 3 June 2025
Tagged: software

A "web application" is a system. It has a backend, maybe a database, user accounts, cookie consent, analytics. Probably the frontend uses some sort of build system or web packer, maybe TypeScript, almost certainly has a multi-hundred-megabyte node_modules directory. But not everything has to be like this. You don't need all this stuff. I think there is an easy trap to fall into where you go down the path of starting projects with create-react-app and paint yourself into a corner of ever-increasing complexity. In this post I'm going to argue for web programs as a better way.

Of course there are web applications that genuinely are big applications that need a backend, a database, user accounts. If you're building something like this... carry on. I'm not trying to tell you what to do.

But if you're building something that could run entirely locally on the user's computer: then I'm talking to you, and I have some suggestions for how to do it well.

A web program is a standalone static site, made out of HTML, CSS, and JavaScript, with straightforward source code, minimal dependencies, no backend, and no build system.

Values

What kind of software future do we want? Do we want a future where software is opaque, controlled by third-party corporations, temporarily accessed by users, and disappears if it's not profitable? Or do we want a future of software that works like tools, books, and recipes; where users keep what they want, change what they don't, and share it on; where the software lives on even after its creator disappears? Do we want to lease software or own it?

Some concrete values we might seek:

If these values don't matter to you, then "web programs" might not matter to you either. That's fine.

Principles

1. Minimise dependencies

Try to avoid using external dependencies. They increase the percentage of your program that you don't understand, and that is not convenient for you to change.

Obviously sometimes you need an external dependency. Maybe you're making something with a map, I recommend MapLibreGL, try to pick external dependencies that are small and that don't require you to set up an account or create an API key.

2. Self-host everything

If you really must use an external dependency, such as a JavaScript library, then see if you can copy it into your git repository. Don't load it from a CDN.

If you load an external dependency from a CDN, then you have also added a dependency on the CDN. Your program no longer works offline. If the CDN stops working, your program stops working. Also the CDN operator gains some level of ability to monitor the users of your program. Also if the CDN is compromised and starts shipping bad code, your program is compromised (modulo subresource integrity).

If you really must use an external CDN, then obviously use subresource integrity, and also try to pin to a specific version instead of a "latest" tag. No matter how hard the developer swears they will only introduce backwards-compatible changes.

3. Eschew build systems

Any build system adds friction when you need to set up a new development environment. In particular it adds friction for users who want to make changes to your program because now they have to figure out not just how to change the source, but also how to build it.

It also means that "View source" in the browser is not going to show them the actual source. And I don't want to hear about "source maps". You don't solve a problem of complexity by adding more complexity! You solve it by taking complexity away.

4. Use relative paths

For example, you should refer to "js/util.js" rather than "/js/util.js", and definitely rather than "http://example.com/js/util.js".

If you use relative paths then you can copy your program to a different directory and it will still work.

If you try to load a script from "/js/util.js", but the page is running from "http://example.com/my-cool-web-program/" then the browser will try to load the script from "http://example.com/js/util.js" instead of "http://example.com/my-cool-web-program/js/util.js".

5. Work from "file://" URLs

Users should be able to clone your git repository, disconnect from the internet, open up "index.html" in the browser, and have the program work just as well as it does on your website.

Obviously this means using relative paths and not assuming a web server. But it also means you can't use some web features because for whatever reason they're not available from "file://" URLs. You can't use:

If you're using fetch()/XMLHTTPRequest to load some data source, then the workaround is to store the data in a JavaScript variable and load it with a script tag.

Bad:

// foo.json:
{"hello":"world"}
 
// main.js:
const r = await fetch('foo.json');
const fooJsonData = await r.json();

Good:

// foo.js:
const fooJsonData = {"hello":"world"};
 
// index.html:
<script src="foo.js"></script>

Instead of ES modules, load every JavaScript file using script tags.

Bad:

// bar.js:
export class Bar {
    ...
}
 
// main.js:
import { Bar } from './bar.js';
...
const bar = new Bar();
 
// index.html:
<script type="module" src="main.js"></script>

Good:

// bar.js:
class Bar {
    ...
}
 
if (typeof window === 'undefined') {
    // Node.js or ES modules
    export { Bar };
} else {
    // script tags
    window.Bar = Bar;
}
 
// main.js:
const bar = new Bar();
 
// index.html:
<script src="bar.js"></script>
<script src="main.js"></script>

Instead of using export, assign to window.$ClassName, and make sure your script tags load your dependencies before their first use. If your classes have a straightforward dependency graph this should be easy. And we're trying to make things simple, so you should have a straightforward dependency graph.

As a bonus, if you use a snippet like in the "Good" version of bar.js, your class will also work in an ES modules environment, so you can still use it in NodeJS if you really want to.

Instead of cookies, you can use localStorage, or better yet (depending on the nature of the thing you're storing), let the user save it to disk and load it from disk. That puts the data closer to the hands of the user, where they can more easily edit it, copy it to other places, import/export to other software, or just look at it and learn something.

6. Be pragmatic

Pragmatism is the most important principle of all! Always be skeptical of prescribed principles. Principles are handed down from an ivory tower, from someone who is not looking at the exact problem that is facing you. Use your judgment, throw away the principles if they are not helpful.

I'm offering advice on how to build simple web programs, not dogma to live your life by.

Benefits

If you follow these principles, you get:

Make life easy for yourself, both as a developer and a user. You won't be fighting with npm. You won't be debugging complicated minified code inside dependencies you didn't even know you had.

Reduce hosting costs. You don't even need to deploy an entire Docker container per project. Clone your multi-kilobyte git repository on to your web server and symlink it into the document root. You can host thousands of web programs on the cheapest VPS you can find. And your web programs will scale pretty far before you need more than the cheapest VPS you can find, because the only backend you need is nginx.

Run on air-gapped computers. If your program is simple enough to run out of a "file://" URL, then it becomes easy to copy it to an air-gapped computer and run it there.

Host on IPFS. If your program isn't too fussy about URLs, it should be work on IPFS with no changes required.

Reliability. You don't need to keep up with the ever-changing JavaScript landscape. You don't need to keep your backend online. You don't need to make sure the database isn't running out of space.

GDPR compliance. You don't need to worry about data breaches if you don't have any data.

Education. Curious users can "view source" your application and understand how it works. Don't underestimate how powerful that is. Many of us got into programming in the first place by just mucking around with the stuff we were already using and seeing what we could do with. Nowadays web applications are minified at best and obfuscated at worst, but we can choose to purposely make our code discoverable, and if we're lucky we might light the spark of curiosity in the next generation. If we have knowledge that we don't pass on to the next generation before we die, then that knowledge dies with us. If we make a habit of letting knowledge die with us, civilisational downfall is guaranteed.

Customisation. If the program is easy to understand and easy to modify, users can more easily modify it to suit their needs. Imagine how easy it is to modify a "Hello world" page versus Google Docs.

"Save page as" makes a working copy of your program if your program is just straightforward HTML, JS, and CSS that works out of a "file://" URL.

Every environment is a development environment. You'll never again waste a morning on setting up a development environment. If you have a browser and a text editor, you have a development environment.

Conclusion

We've let web programming become a ceremony, and it doesn't have to be. A web program is just HTML, CSS, and JavaScript, and that's all it ever needed to be. If you're writing code for yourself, your friends, or people who might "View source" and learn something: take the simpler path. Your programs will work better and last longer.

Related



If you like my blog, please consider subscribing to the RSS feed or the mailing list: