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.