things you can fit in a single shell script
things you can fit in a single shell script
there are two kinds of programmers. the ones who look at a framework and think “i could build a lighter version of this.” and the ones who look at a framework and think “i could build this in bash.” this post is for the second group.
autocrud
autocrud is 120 lines of bash. it turns any SQLite database into a REST API. no config. no schemas. no routes file. no migrations. no ORM.
DB_FILE=./mydb.db socat TCP-LISTEN:8080,reuseaddr,fork EXEC:./autocrud
that’s the whole server. socat pipes TCP connections to a bash script. the script reads the HTTP request, extracts the table name from the URL, runs the SQL, returns JSON. every table gets full CRUD automatically.
GET /api/users -> SELECT * FROM users
GET /api/users/1 -> SELECT * FROM users WHERE id = 1
POST /api/users -> INSERT INTO users ...
PATCH /api/users/1 -> UPDATE users SET ... WHERE id = 1
DELETE /api/users/1 -> DELETE FROM users WHERE id = 1
query parameters map to SQL.
?select=name,email&order=id.desc&limit=10&name=like.Ali
becomes
SELECT name, email FROM users WHERE name LIKE '%Ali%' ORDER BY id DESC LIMIT 10.
the URL is the query.
it reads pragma_table_info to discover columns. it
matches JSON keys to column names. extra keys are ignored. missing
columns get SQLite defaults. any schema works because it doesn’t know
your schema. it asks the database.
this idea is not original. PostgREST figured it out in 2014 – 40,000 lines of Haskell. Hasura took it further – GraphQL, subscriptions, raised over a hundred million dollars. autocrud is what happens when you strip all of that away and ask: what’s the absolute minimum? the answer is one shell script, socat, and sqlite3. all three have been on every unix box for decades.
what i like about it isn’t that it’s useful. it is useful, for prototypes and hackathons and raspberry pis. what i like is that it exposes what a backend actually is. the database knows the schema. the URL encodes the intent. everything else – controllers, models, routes, query builders, ORMs – is ceremony. autocrud removes the ceremony and leaves the skeleton. 120 lines of skeleton.
nest.shell
nest.shell
is autocrud’s louder, funnier, more ambitious sibling. it’s a NestJS
parody written entirely in bash. file-system routing. dependency
injection (it’s just source). guards (.acl
files). pipes (validation scripts). controllers (.api.sh
files). modules (folders). a CLI with
nest generate controller cats. 43 tests, all passing. OWASP
security hardening. a C-based TCP shim. a pre-fork worker pool. all
under 700 lines of bash.
nest new my-app
nest generate module dashboard
nest generate controller cats
nest start
the CLI is NestJS-compatible in the way a funhouse mirror is
compatible with a person. it’s the same shape. the proportions are
wrong. the nest generate controller cats command creates
content/api/cats/ with list.api.sh,
create.api.sh, get.api.sh. these are bash
scripts that receive $HTTP_METHOD and
$REQUEST_BODY as environment variables. they call sqlite3
and return JSON. this is not an approximation. this is NestJS,
deconstructed, reconstructed, and slightly concussed.
the benchmarks are honest. 17 requests per second. nginx does 50,000. the README includes a chart with a single pixel-wide bar labeled “nest.shell (bash)” next to nginx’s solid block. “No. Next question.”
the security section is genuine. path traversal blocked. SQLi detection with structured logging. HTML entity escaping. unicode sanitization that strips zero-width characters, RTLO bidi overrides, ZWJ, skin tones, BOM, variation selectors. security headers on every response. a token-bucket rate limiter. this is not a joke framework with joke security. the security is real. the framework is the joke.
the C files are the punchline within the punchline.
nestsrv.c is 14KB – a custom TCP server that replaces
socat. nest-prefork.c is an nginx-style worker pool.
persistent bash workers receive requests via pipes instead of forking
per request. it’s a theoretical 5x speedup. it’s also a C program whose
entire purpose is to make a bash web framework slightly less slow. this
is the kind of commitment to the bit that i respect.
what this is about
both projects are code golf. not the competitive kind where you squeeze a quine into 32 bytes. the other kind. the kind where you ask: what’s the smallest thing that’s still recognizable as the thing?
autocrud asks: what’s the smallest REST API? the answer is one file, two dependencies, zero configuration. you can understand the entire codebase during a coffee break. there are no abstractions to peel back because there are no abstractions. the bash is the framework. the sqlite3 is the ORM. the filesystem is the deployment.
nest.shell asks a different question: what’s the smallest thing that’s still recognizable as NestJS? the answer is roughly 700 lines of bash, a straight face, and the willingness to ship a project whose benchmark chart is a self-own. but here’s the thing: the architecture survives. modules, controllers, dependency injection, guards, pipes – all the patterns NestJS popularized. they don’t require TypeScript. they don’t require decorators. they don’t require node. they’re just patterns. a folder is a module. a bash script is a controller. the patterns are the framework. the language is an implementation detail.
this is deconstruction in the literal sense. taking something apart to see how it works. nest.shell is NestJS with all the language-specific machinery removed, leaving behind the skeleton of ideas. and the skeleton still works. badly. but it works.
the humor as method
the nest.shell README is funnier than most tech blogs. the benchmarks are a joke that also contains real data. the license: “Copyright (c) 2024, someone who looked at node_modules and said what if bash. All rights reserved. Especially the right to be ridiculous.” the FAQ: “Can I use this in production? Technically yes. Morally? That’s between you and your incident response team.”
this kind of humor is doing work. it’s signaling that the project doesn’t take itself seriously, which gives the reader permission to not take it seriously either. which means the reader can engage with the ideas without feeling like they’re being sold something. the humor is a doorway. you come for the joke and stay for the architecture.
i think this is underrated as a communication strategy. technical writing defaults to either the academic register (neutral, precise, bloodless) or the marketing register (enthusiastic, breathless, selling). humor is a third option. it lowers the stakes. it makes space for saying “i built this ridiculous thing” without having to also say “and you should use it.” the reader can decide for themselves what’s useful and what’s just funny. the author doesn’t have to separate the two.
autocrud and nest.shell are both ridiculous. they’re also both real. they both work. they both taught me something about what a framework actually is and how little you actually need. the joke and the insight are the same thing.
the deconstruction
here’s what i learned, building these:
a web framework is five things: routing, request parsing, response formatting, dependency wiring, and some convention for organizing code. everything else – ORMs, validation libraries, template engines, middleware stacks, CLI generators – is accretion. useful accretion, often. but not essential.
autocrud has no routing. the URL is the routing. nest.shell’s routing is the filesystem. both work because the problem of “which code handles this request” has a simpler answer than we usually admit: the request tells you. the path tells you. the method tells you. you don’t need a route table. you need a convention.
dependency injection is source utils/whatever.sh. it’s
not elegant. it pollutes the global namespace because bash doesn’t have
namespaces. but it works because bash scripts are small. when your
entire application is a few hundred lines, you don’t need a DI
container. you need to know which files depend on which other files, and
source makes that explicit.
guards are .acl files that get sourced before the
controller runs. if they exit 1, the request is denied. this is the same
pattern as Express middleware, NestJS guards, or Django decorators. the
implementation is different. the pattern is the same.
the takeaway isn’t “build your next startup in bash.” the takeaway is that the distance between a framework and a shell script is smaller than it looks. the patterns survive. the language doesn’t matter. the ceremony is optional.
links
- autocrud – any SQLite database, instant REST API. 120 lines of bash.
- nest.shell – a bash web framework. NestJS-compatible. 17 req/s.
- PostgREST – the real thing. 40,000 lines of Haskell.
- SQLite – the best software ever shipped.