13·06·2026
I’ve always been interested in coding as a craft - a thing to do with your hands, your eyes and your mind. In many ways, I feel that a lot of the satisfaction and accomplishment I get from making software comes from the process itself, the doing of it, and not necessarily the end result. To quote one of my favorite musicians:
Most vagabonds I knowed don’t ever want to find the culprit
That remains the object of their long relentless quest,
The obsession’s in the chasing and not the apprehending,
The pursuit you see and never the arrest.
- Tom Waits, [Foreign Affair]
This is also one of the reasons why the recent AI “revolution” doesn’t really resonate with me. They say about us software developers that we always are looking to automate our workflows, but personally this is not what’s driving me. I don’t mind spending a few more minutes on writing another REST controller, or another Javascript keyboard event handler, or just copying files and manually setting up a new server. And I find in many cases I spend more time thinking about a problem than actually coding the solution!
To me this is all part of the process, it’s all enjoyable, and since I’m my own boss, I have the luxury of taking my time. My clients know that I’m dependable and available when there’s a problem, and that I get the job done, so I can concentrate on the process itself, and not worry so much about velocity.
This is also why I like to make my own tools, instead of just blindly relying on some ready-made frameworks or libraries. It’s not only about freedom, it’s also about the joy of creation, and the deeper understanding and knowledge of the lower-level aspects of the system I’m building, be it parsing HTTP requests, putting together SQL queries, issuing system calls, forking, trapping process signals etc.
Are We All Just CRUD Monkeys? #
Now don’t get me wrong, I totally understand the value of using frameworks such as Ruby on Rails, and the ecosystem that goes with it. Rails provides teremendous value to developers, and an infrastructure for building web apps in big teams. And it also demands a deep knowledge, but of a different kind. Instead of learning about low-level stuff, you learn about what solutions already exist for the problem you’re trying to solve, and how to integrate them into your app. It’s sort of like the difference between knowing how to properly use and sharpen a wood chisel, and knowing how to properly use and maintain a tenoning machine.
It’s perfectly fine to build yourself a career on top of Ruby on Rails, call yourself a CRUD monkey, and now with the help of AI technologies you can have an entire army of CRUD monkeys at your service. DHH says he’s built an entire career on being a CRUD monkey, but in a way that’s not true. Rather, he’s created Ruby on Rails (of course, with the help of many other contributors), which is a great piece of software that in many ways has revolutionized web development. So perhaps it’s more accurate to say that he made a career on turning other people into CRUD monkeys. But I think for him also, there’s the joy of working on Ruby on Rails itself, and not only using it to make CRUD apps.
Ruby is not Only Rails #
One big problem in the Ruby community is the somewhat disproportionate place Rails has in the ecosystem. While there’s still a lot happening in the community off the Rails, it’s prettyclear that most of the activity is still around Rails. At the last Euruko conference in Viana do Castelo, by my count more than half of the talks were about Rails or tools that integrate with Rails. In and of itself this is not necessarily a bad thing, but I found it a bit sad that there was no presence (as far as I could tell) for any other web framework: no Roda, no Hanami, no Sinatra. Maybe this is just the nature of things, but is it really desirable?
Rails is incredibly powerful, it provides so much value out of the box. But there are other approaches possible, and maybe Rails can even learn from them. There are precedents: Merb was a simpler and faster alternative to Rails, before being merged into Rails. Sequel has shown developers that there’s a better way to work with databases, and then ActiveRecord got Arel. So Rails can actually gain from more innovation in the Ruby ecosystem.
A First Look at Syntropy (part I) #
So with that in mind, I’d like to share with you a first look at the web framework I’m currently working on, which I call Syntropy, and which actually is what drives this website.
I chose the name Syntropy because in the last few years I’ve been passionate about
[Syntropic Agroforestery]as an approach to agriculture (I’m an amateur gardener), and to the cultivation of rich and diverse ecosystems that are also beneficial to human beings.
Syntropy is kind of the inverse of entropy. While entropy is the natural tendency of a given system towards disorder and decay and homogeneity, syntropy is the power of life to create order out of chaos, to concentrate energy and matter in various life forms, and to create more and more diversity and abundance.
When you think about it, life is the only thing in the universe that can > defy entropy - it can create forms, it can concentrate energy and matter in living things. Syntropy is an approach that takes that observation and applies it to agriculture: how can we harness this power of life to overcome entropy, and how we can work with ecosystems to create more richness, more diversity, > and more abundance, not only for human beings, but for
allliving things?This is why I’m so passionate about this concept, because it offers us not only a vision of true abundance, but also an alternative to our current social, political and economic structures that have a “zero-sum game” approach.
Syntropy is about living, abundant ecosystems teach us that in fact each of us is valuable to our societies and that sharing in the abundance just brings more abundance. Food for thought…
My original inspiration for Syntropy came from Jekyll - a static site generator, which I have used previously for my blog. In Jekyll, like all other SSGs I guess, the URL structure of the website mirrors the directory structure of the source code. To illustrate what I mean, we can take the following directory structure and the corresponding mappings to URLs:
files URLs
===== ====
+ site/
+ index.html /
+ about.md /about
+ blog/
+ 2025-01-foo.md /blog/2025-01-foo
+ 2025-02-bar.md /blog/2025-02-bar
Now in the above example we just have a few HTML and Markdown files, but we can also have additional “asset” files such as CSS, JS and images. You can also see that the routing scheme is very easy to understand: URLs are hierarchical in nature, and the fact they mirror the directory structure makes it trivial to organize the different pages of the site.
So the question I asked myself was: what if I had a tool like Jekyll for creating websites but with support for dynamic content? What would be the implications of that for the way the source code is organized? And how does this relate to the Model-View-Controller pattern?
File-Based Routing #
I spent some time researching existing solutions for this in Ruby or in other programming languages. First of all there’s Bridgetown, a Ruby-based SSG which also offers the possibility of adding dynamic content via Roda, which means that you can make hybrid sites that use markdown for static content, and Roda for the dynamic parts. You do need to express the dynamic routing with a Roda dynamic tree, which is expressed in code. So, you can only use file-based routing for the static parts of your site.
It should also be noted that PHP web apps by default use file-based routing, but if you want to implement some more advanced patterns, such as clean URLs, or maybe parametric routes, you’ll probably need to fiddle with your HTTP server’s configuration (be it Apache, Nginx, Caddy or something else.)
I’ve also looked at a few different Javascript routers that use file-based routing, such as Expo, Tanstack Router, React Router, and finally SvelteKit.
I think out of all of those different solutions the most comprehensive one is
the SvelteKit router, but it does have some peculiarities: all routes must be
defined as directories, and for each route there’s a convention for filenames
for different aspects of route controllers. Because SvelteKit deals with both
frontend and backend concerns, you need to put the frontend code and backend
code into separate files, namely +page.js
and +page.server.js
. You can also create files for layouts and error handling, and for me with all those files it gets a bit confusing. But On the other hand svelte kit introduces some useful conventions for file-based routing:
- Files starting with an underscore (e.g.
_foo.js
) are considered internal and not exposed by the application. - Parametric routes are defined using square brackets, e.g.
foo/[id].js
. - Index files handle requests to their containing directory, so
/foo/index.js
points to/foo
.
Code Organization and Classless controllers #
Another problem I had to think about was how to organize the controller code. Let’s take as an example a blog-type website, where all the pages are dynamic:
files URLs
===== ====
+ site/
+ index.rb /
+ about.rb /about
+ rss.rb /rss
+ blog/
+ index.rb /blog
+ new.rb /blog/new
+ [slug] /blog/[slug]
How should these controllers look? Do we define some uniform interface? How do they get loaded? What if you need to compose them in some way? How do you deal with middleware?
The design I came up with works as follows:
- The URL structure matches the directory structure of the app.
- Ruby source files are considered dynamic controllers that are loaded as modules (more on that later) and handle requests according to their path.
- Markdown files are served as HTML, with support for layouts and metadata.
- Anything else is static files: CSS, JS, media files etc.
index.rb
handles its immediate directory, e.g.app/about/index.rb
handles/about
.foo.rb
handles the route minus the.rb
extension, e.g.app/about/foo.rb
handles/foo
.- A file name ending with a plus sign handles its entire subtree, e.g.
app/bar+.rb
handles/bar
,/bar/baz
etc. - Parametric routes are handled by specifying the parameter in square brackets
as the file name, e.g.
app/posts/[post_id].rb
handles/posts/42
etc. Parametric routes can also be used on directories:app/posts/[post_id]/edit.rb
will handle/posts/42/edit
. - Any file or directory starting with an underscore will be skipped by the router and will not be exposed by the server.
Module #
So the fact that each dynamic route is basically some kind of controller, makes it necessary to define an interface for those controllers that is both flexible and easy to write. I wanted to avoid having to define a separate class for each controller as one would do in Rails, and that means that each module should be loaded in isolation, inside of a separate context object:
class ModuleContext
def self.load(env, code, fn)
new(env).tap { it.instance_eval(code, fn) }
end
def initialize(env)
@env = env
singleton_class.const_set(:MODULE, self)
end
...
end
We can then write code like this:
export ->(req) { req.respond(foo) }
def foo
'Hello, world!'
end
…where export
sets the entrypoint of the module, in this case a lambda that
takes a request object as argument, and responds with a string. Another way to
write a controller would be with a call
method:
export self
def call(req)
req.respond(foo)
end
def foo
'Hello, world!'
end
Or perhaps with a Papercraft template:
export template { |**|
html {
head {
title 'My awesome app'
}
body {
h1 'Hello!'
}
}
}
And if we have export
, we might want to implement import
as well in order to allow expressing dependencies:
layout = import '/_layout/default'
export layout.apply {
p 'Hi!'
}
This means we can extend the idea of loadable modules and implement any part of our app as a module, be it a controller, a view template, a layout template, a model or any other functionality our app may need.
Explicit Dependencies and Code Structure #
Now that I’m starting to work with Syntropy and to build apps with it, I find
the import/export
module mechanism really powerful. It sort of makes you think about the interface: what is the module supposed to do, and what API does it expose?
Of course, controller modules would have a callable interface, but any other module can provide a singleton object as its interface, or it can export a class, or any other object, such as a Papercraft template, or a configuration hash. It’s left to the programmer to decide how to connect the different parts of the application, but since the dependencies between the modules are explicitly expressed, we can follow the code more easily, and have a clear understanding of the dependency graph.
And since we evaluate the code for each module in its own separate module
context, that means we get complete isolation of each module, so constants and
methods stay local to the module and do not leak to the global context. This
also makes it almost trivial to implement hot-re: with the import
functionality, we can track reverse dependencies, and thus propagate a module re to all dependent modules.
The Syntropy Routing Tree #
So now that we saw how the app’s Ruby modules may look, let’s look at how routing is done. As discussed above, we have the following conventions for file names:
index.rb
handles its immediate directory, e.g.app/about/index.rb
handles/about
.foo.rb
handles the route minus the.rb
extension, e.g.app/about/foo.rb
handles/foo
.- A file name ending with a plus sign handles its entire subtree, e.g.
app/bar+.rb
handles/bar
,/bar/baz
etc. - Parametric routes are handled by specifying the parameter in square brackets
as the file name, e.g.
app/posts/[post_id].rb
handles/posts/42
etc. Parametric routes can also be used on directories:app/posts/[post_id]/edit.rb
will handle/posts/42/edit
. - Any file or directory starting with an underscore will be skipped by the router and will not be exposed by the server.
With those conventions in mind I started working on a router implementation. My main objectives were to build something simple, self contained and that worked fast. When I implement a new feature, or when I do research on possible solutions to a problem, I tend to start at the end, that is I prefer to first work on the programming interface and the usage of the feature, rather than its implementation.
In the case of the router, I wanted to model it as a pure function that takes a URL path as an input, and returns some kind of route object as an output. The found route object contains information on how to process the incoming request, and usually points to some controller entrypoint:
router = ->(path) { ... }
route = router.(request.path)
route[:entrypoint].(request)
So how do we turn a bunch of directories and files into a routing function? the first stage would be to scan all those directories and files and generate an abstract routing tree that represents the app’s file structure.
We can then handle each request by traversing the abstract routing tree and matching entries against the path until we find the target route.
Note that if we didn’t need to support wildcard or parametric routes, we should have been able to statically map clean URLs to filenames, and then our routing algorithm is simply a hash lookup, e.g.:
url_map = Dir[File.join(app_dir, '**/*')].each_with_object({}) { |fn, map|
map[clean_url(fn)] = make_route(fn)
}
router = ->(path) { url_map[path] }
But if our app has wildcard or parametric routes, that means we do need to traverse the abstract routing tree, in order to find the correct route, and that means we need to look at each segment of the request path separately, and match routes against it.
Now, this is not a very hard problem, and we could easily write a tree traversal
algorithm that matches against path segments. But that also means we’ll need to
traverse the abstract routing tree on each request, which may be slow. It
occurred to me that maybe this is one of those situations where code generation
might be helpful. Instead of traversing an abstract routing tree, we can use
case
statements for matching against each path segment. In other words, instead of representing the tree structure in data, we represent it in code.
Consider the following directory structure, implementing a part of GitHub’s URL structure.
+ site/
+ [org]/
| + [repo]/
| + index.rb
| + issues/
| | + [id].rb
| + index.rb
+ collections.rb
+ explore.rb
+ index.rb
We can then design a router that is tailor-made for this specific situation:
->(path, params) {
entry = @static_map[path]
return entry if entry
segments = path.split("/")
case (s = segments[1])
when s
params["org"] = s
case (s = segments[2])
when nil
return @dynamic_map["/[org]"]
when s
params["repo"] = s
case (s = segments[3])
when nil
return @dynamic_map["/[org]/[repo]"]
when "issues"
case (s = segments[4])
when s
params["id"] = s
case (s = segments[5])
when nil
return @dynamic_map["/[org]/[repo]/issues/[id]"]
end
end
end
end
end
nil # no match
}```
The code above uses two hashes for route lookup: a `@static_map` and a
`@dynamic_map`. The dynamic map is used for parametric or wildcard routes. The
static map is used for all other routes: static files and static controllers.
The code first tries to do a static map lookup. If nothing is found it proceeds
to split the given request path into segments, and to match against each
segment.
This technique is remarkabe because instead of traversing the abstract routing
tree, and having to visit the different "leaves" of the tree in order to match
request path segments against them, we instead represent the tree in code as
nested `case` branches for each segment. This technique also performs very well,
surpassing any other design I could come up with.
So, the challenge here is how to generate code that uses this technique, and to
be able to do it for any arbitrary directory structure. The result is the
[`RoutingTree`](https://github.com/digital-fabric/syntropy/blob/main/lib/syntropy/routing_tree.rb)
class. It is built as a completely self-contained component that has zero
dependencies, and it has a functional design: you feed it the path of the app's
root directory, and it gives you back a router proc. That router proc's code is
generated dynamically after first scanning the directory structure.
## Middleware and file-based routing
Once we embrace file-based routing, adding support for middleware just follows
the directory structure: we can add support for `_hook.rb` files that
implement middleware. These handlers will be invoked for any routes in their
subtree. For instance, `app/posts/_hook.rb` will be invoked for requests to
`/posts`, `/posts/42`, `/posts/42/edit` etc.
The interface is remarkably simple - the middleware hook is simply a proc (or
callable) that accepts two arguments: the request object and the route handler
proc. Here's how an authorization middleware hook may look:
``` ruby
export ->(req, proc) {
if !validate_auth(req)
req.redirect('/signin')
else
proc.(req)
end
}
The fact that the scope of the middleware is determined by its location in the app’s directory structure also means that we can easily compose them, such that each route will invoke all middleware hooks found up its file tree, and those hooks will be executed in order from the most general (i.e. starting at the root) to the most specific (ending in the route’s containing directory.)
Error handlers #
The same goes for error handling - you can place error handlers anywhere in the
app’s directory structure and each of them will apply to its subtree, such that
/app/posts/_error.rb
will handle raised exception in requests to any route
under /posts
but not to e.g. /users
. When an exception is raised and not rescued, it will bubble up to the first error handler found up the tree, so for example you could have a general error handler that renders the error message with your app’s layout and styling, but also have an error handler for your app’s API that returns a JSON response:
layout = import '/_layout/default'
export ->(req, error) {
req.respond_html(
layout.apply {
h1 "Error: #{error.class}"
p error.message
},
':status' => error.http_status
)
}
export ->(req, error) {
req.respond_json(
{
error: error.class,
message: error.message
},
':status' => error.http_status
)
}
Common Controller Patterns #
Now that we discussed the outer architecture of a Syntropy application - consisting of file based routing and loadable modules, with support for middleware and error handling, how do we actually write controllers? What do they look like? The fact that Syntropy does not impose any structure on your controllers may be daunting at first, but in fact Syntropy provides all the tools needed for writing controllers.
If we look at the different kinds of controllers an application may have, some common patterns emerge:
CRUD controller: this is the typical Rails controller, which actually handles a few different URLs. If we had aPostController
, it would actually handle/posts
,/posts/new
,/posts/[id]
and/posts/[id]/edit
, and the different actions are also related to different HTTP methods.REST controller: this type of controller handles a single URL endpoint, but its action differs according to the HTTP method used.** JSONRPC controller**: this controller only handles POST requests and typically will do some kind of dispatch to an internal handler.** Plain GET controller**: this controller just handles GET requests by rendering HTML or JSON.
For each of these controller types, we can easily come up with simple, low-level constructs. For example, the REST controller may be expressed as follows:
export dispatch_http_method
def get(req) = ...
def post(req) = ...
def delete(req) = ...
A JSONRPC controller may look as follows:
export dispatch_json_rpc
def add(x:, y:) = x + y
def mul(x:, y:) = x * y
A plain GET controller may look as follows:
template = import '/_views/foo'
export ->(req) {
req.validate_http_method('get')
req.respond_html(template.render)
}
In fact the open design of Syntropy modules makes it quite easy to create all
kinds of controller abstractions. While Ruby on Rails relies on scaffolding and
an ApplicationController
class as the base for handling interactions with HTTP clients, Syntropy gives you simple, composable building blocks you can apply to your app’s requirements.
Syntropy - Freedom and Abundance #
So this was a first look at Syntropy. In the next few articles I’ll publish here I’ll continue discussing Syntropy’s design as it relates to dealing with databases and storage. Until then, please feel free to poke around the Syntropy repository: https://github.com/noteflakes/syntropy/.