Jimmu (神武)
A lightweight Ruby web framework. Full-featured but simple: ERB, SQLite, file-based routing, and WebSockets in one file, so you can go from nothing to a working small web app in minutes.
gem install jimmu
jimmu new myapp
cd myapp
jimmu server
Then visit http://127.0.0.1:3000/.
Why
- One file. The entire framework is
lib/jimmu.rb. Nothing to configure, nothing to generate boilerplate for. - No native gem dependencies. SQLite support talks directly to your
system's
libsqlite3via Ruby's built-inFiddlelibrary -- there's nosqlite3gem, so there's no native extension to compile when you install. Everything else is Ruby standard library. - A file is a page. Drop a
.erbfile underviews/and it's routable. No router config, no controller classes.
Installing
gem install jimmu
Requires Ruby >= 2.7 and a system SQLite3 shared library, which ships with
macOS and is available via your package manager on Linux
(apt install libsqlite3-0, dnf install sqlite-libs, etc.) and typically
already present with Ruby installations on Windows.
The CLI
| Command | Effect |
|---|---|
jimmu new <name> |
Create a new project in ./<name> |
jimmu server |
Start the dev server, reading ./app.rb |
jimmu server -p 8080 |
...on a specific port (overrides port in app.rb) |
jimmu server -h 0.0.0.0 |
...on a specific host (overrides host in app.rb) |
jimmu version |
Print the installed version |
jimmu help |
Show usage |
Project layout
myapp/
├── app.rb # entry point -- config, hooks, then `run`
├── views/
│ ├── layout.erb # auto-applied around every page unless disabled
│ ├── index.erb # -> /
│ ├── 404.erb # shown on 404
│ └── 500.erb # shown on unhandled errors
├── public/
│ ├── css/style.css # -> /css/style.css
│ ├── js/app.js # -> /js/app.js
│ └── img/
└── db/ # SQLite files live here by convention
File-based routing
A file's path under views/ becomes its URL. index.erb files match their
parent directory:
views/index.erb -> /
views/about.erb -> /about
views/blog/index.erb -> /blog
views/blog/post.erb -> /blog/post
Square brackets make a segment dynamic and land in params:
views/user/[id].erb -> /user/:id params[:id]
views/post/[category]/[id].erb -> /post/:category/:id params[:category], params[:id]
A literal file always wins over a dynamic one at the same level (so
views/user/admin.erb would take priority over views/user/[id].erb for
/user/admin specifically, while every other value still hits the dynamic
route).
Every HTTP method routes through the same file -- check request.method
inside the template to branch between, say, showing a form on GET and
handling its submission on POST to the same path.
Inside a view
Each .erb file has access to:
params[:id] # route + query + form + JSON body params (merged)
request.method # "GET", "POST", ...
request.path # "/user/42"
request.ip # client IP
request.header("user-agent") # any request header, by lowercase name
request.query_params # just the ?query=string part
[:theme] # read a cookie
[:theme] = "dark" # queue a Set-Cookie
session[:user_id] # read/write server-side session data
# (survives across requests via a cookie;
# cleared with session.clear or session.delete(:k))
status 404 # set the response status (doesn't stop rendering)
header "X-Custom", "value" # set a response header
redirect "/login" # 302 and stop
json(ok: true) # render JSON and stop
text "plain text" # render text/plain and stop
html "<b>hi</b>" # render text/html and stop
render "other_view" # render another view (through the layout) and stop
layout false # skip views/layout.erb for this render
log "message" # timestamped line on the server console
db # the most recently opened `sqlite` connection
redirect / json / text / html / render all end the current
template immediately -- nothing written after them in the same .erb file
runs.
Layouts
views/layout.erb wraps every page automatically; put <%= yield %> where
the page's own content should go. Turn it off per-page with <% layout false %>
anywhere in that page's template.
Errors
views/404.erb renders on an unmatched route; views/500.erb renders on an
unhandled exception (it can read error.message / error.backtrace if you
want to show details, or just show a friendly message). You can also trigger
either one yourself:
<% unless post
status 404
render "404" # note: a string, not :404 -- see "Gotchas" below
end %>
If neither file exists, Jimmu shows its own built-in page -- a plain message for 404, and a full backtrace for 500 (since that's only ever seen when you haven't set up your own error page, it defaults to maximally useful rather than maximally polished).
SQLite
# in app.rb
db = sqlite "db/app.db"
db.exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
<%# in any view %>
<% users = db.query("SELECT * FROM users WHERE name = ?", params[:name]) %>
<% db.exec("INSERT INTO users(name) VALUES (?)", params[:name]) %>
<% db.transaction do
db.exec("...")
db.exec("...")
end %>
query returns an Array of Hashes with Symbol keys. exec returns the
number of rows changed. db.last_insert_rowid gives you the last
auto-increment id. ? placeholders are the only supported binding style;
always use them for anything that includes user input.
File uploads
<form method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
</form>
<% file = params[:avatar]
file.filename # "photo.png"
file.content_type # "image/png"
file.size # bytes
file.save("public/uploads/photo.png") %>
WebSockets
Registered in app.rb, separately from the file-based view routes:
websocket "/ws/[room]" do
log "opened: #{params[:room]}"
{ |msg| send("echo: #{msg}") }
on_close { log "closed" }
end
Inside the block (and inside on_message / on_close), self is the
connection, so send, close, params, session, cookie, request,
log, and db are all available directly. There's no special "broadcast"
method -- see examples/chatroom and examples/poll for the few lines of
plain Ruby (a shared Array/Hash + Mutex) it takes to build one yourself.
Hooks
before do
log "#{request.method} #{request.path}"
end
after do
header "X-Powered-By", "Jimmu"
end
before runs ahead of every request (including static files); calling
redirect/json/etc. inside one stops the request right there, which is
enough to build things like auth gates. after runs once a response body
has been produced.
Gotchas
render "404", notrender :404.:404isn't valid Ruby (a bare symbol can't start with a digit) -- use a String for view names that start with a number.- Helpers belong at the true top level of app.rb.
def my_helper/MY_CONST = .../module Helpers ... endwritten directly in app.rb are ordinary top-level Ruby and are callable from anywhere -- views,before/after,websocketblocks. (Concretely: app.rb isloaded as a normal file, not stringwiseeval'd, specifically so this works.) - Sessions are in-memory. They reset when the server restarts. Fine for development and small apps; swap in a database-backed store yourself if you need durability.
- One request per connection. Keep-alive isn't implemented -- each HTTP request gets its own TCP connection, which keeps the server simple. This doesn't apply to WebSocket connections, which stay open as long as the client does.
- No chunked request bodies. Incoming requests are read via
Content-Length; chunkedTransfer-Encodingon requests isn't parsed (this practically never comes up for browser form posts orfetch()).
Examples
Twelve complete apps in examples/, each jimmu server-able as-is:
| Example | Shows off |
|---|---|
hello-world |
The absolute minimum, plus one dynamic route |
bbs |
A message board -- SQLite, forms, session-remembered name |
chatroom |
Named chat rooms over WebSocket, with a hand-rolled broadcast |
todo |
SQLite CRUD via small per-action routes |
blog |
Posts list/detail, /post/[id], a create form, custom 404 |
counter |
A JSON API endpoint called from client-side fetch() |
url-shortener |
Dynamic redirect route, plus a JSON API to create links |
visitor-counter |
before hook + cookie to tell new vs. returning visitors |
image-upload |
multipart/form-data, saving into public/, a gallery |
poll |
WebSocket used to push live results to every open tab |
json-api |
A headless JSON REST API (no HTML views at all) |
login-demo |
Session-based auth with a before-hook route guard |
cd examples/chatroom
jimmu server
(If you're running straight from this source checkout rather than an
installed gem, use ruby -I../../lib ../../exe/jimmu server instead, or
gem install --local the built .gem first.)
License
MIT. See LICENSE.