Joy: a full stack web framework small enough to host in Janet
A full stack web framework written in janet
At a glance
- What is it?
- Joy is a full stack web framework for the Janet programming language, with routing, a SQLite migration tool, generators for controllers and a CLI that scaffolds a project in one command. It suits a developer who wants a Lisp style stack with a tiny footprint, and its release history shows it has not moved much since 2020.
- Who is it for?
- Joy fits a Janet developer who wants routing, a migration tool and a database connection without pulling in an ORM or a framework with a large dependency tree, since the whole stack is one language and the CLI generates a working project in a couple of commands.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly Janet, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The smallest possible example is a route, a function and a server
Joy is a full stack web framework written in Janet, and the whole introduction fits in a dozen lines. You use the joy module, register a route, write a function that takes a request and returns a response, build the application and start the server on a port.
(use joy)
(route :get "/" :home)
(defn home [request]
(text/plain "You found joy!"))
(def app (app))
(server app 9001)Two details carry most of the design. The route is registered by keyword, with a method, a path and a name, and the handler is a plain function. The response is chosen by calling something like text/plain with a content type and a body, which reads as a content negotiation helper rather than a response object you construct. There is no class to subclass and no object graph to wire. The server call takes the application and a port, so the same app value is what you would hand to a test, which is a good sign for a framework that wants to be exercised without binding a socket.
Installing joy means installing the Janet toolchain first
The setup path assumes Janet is already installed, and then adds the joy CLI through the Janet package manager, jpm.
jpm install joyThe CLI is expected to land on your path. The project anticipates the case where it does not, and the fix given is a line in your .zprofile for people using homebrew, where the path to the janet binary is versioned inside the Cellar directory.
export PATH=/usr/local/Cellar/janet/<your janet version here>/bin:$PATHThat change only takes effect once the profile is reloaded.
source ~/.zprofileScaffolding is then a single command that creates a directory for a new project.
joy new my-joy-projectThe result is a directory with a few files to get you started, and the project is run from that directory with the server command, which starts an HTTP server listening on port 9001 locally.
joy serverThe database is opt-in, and the template does not assume you want one
The interesting default is that the generated project does not include a database, and the documentation is explicit about it. You create one with a dedicated command, which produces an empty database named dev.sqlite3.
joy create dbThen you have to connect it yourself in main.janet, and the three lines shown are the whole change, connecting, starting the server and disconnecting.
; # main.janet
(defn main [& args]
(db/connect (env :database-url))
(server app (env :port))
(db/disconnect))The environment lookup for the database URL is a small but meaningful choice. Configuration comes from the process environment rather than a file the framework parses, which keeps the deployment story simple and makes it easy to point the same code at a different database. SQLite is the database the generator creates, and the repository topics list sqlite as well, so SQLite is the first-class path even though the connection layer is not obviously limited to it.
Migrations are files in db/migrations and a schema.sql you can read
Schema changes are handled by a generator that writes a migration file rather than by a runtime schema builder. You name the table and the columns, and the command creates one file in the db/migrations folder that is waiting to be applied.
joy create table account 'email text unique not null' 'password text not null'The column definitions are passed as quoted strings, so the syntax is close to the SQL you would write by hand, with the type, the constraint and the nullability all in the same string. Applying the migrations is a separate command.
joy migrateRunning it migrates the database and creates a new file, db/schema.sql, which means the resulting schema is dumped as plain SQL you can read, diff and check into source control. That is a low-tech approach and a welcome one compared with a schema that only exists as code in a migration framework. The whole flow is file based, so the database state on disk and the migrations folder travel together and a deployment can be reconstructed by running the migrations against an empty file.
No ORMs, no classes, no objects, just functions
The framework states its position on the data layer directly: in joy there are no ORMs, no classes and no objects, just functions that take requests and return responses. That sentence is a design decision rather than an omission, and it defines what the library is competing with. A request handler is a function, a route is a mapping, and the database is reached through a db namespace with a connect and a disconnect. For a Lisp, that is the expected shape, and for someone tired of entity abstractions it is refreshing. It also means there is no query builder to learn, so the cost lands on you when the queries get more complex, and there is no lazy loading or identity map to reason about, because there is nothing to lazy load. Given the SQLite orientation and the file based migrations, the framework appears to assume you are willing to write SQL.
Controller generation gives you routes and a request handler skeleton
Once a table exists, the CLI can generate the routes for it, which saves you from writing the plumbing by hand.
joy create controller accountThat command creates a new file, routes/account.janet, and updates your main.janet with an import statement so the account routes are wired up. The result is browsable, since the documentation points you at the accounts path on the local server. The generated file is the place to see the framework's conventions, because it shows the shape of a request handler and how a controller is expected to be organised, and it is easier to read the generated code than to imagine it from the documentation. The routes directory and the public directory in the repository layout suggest a similar split for static assets, and the docs directory holds the fuller reference under a readme.md file.
The stated goal is dozens of sites on one cheap VPS
The reason section is short and specific. The author wanted something that felt like coast, another project by the same author, but that took so few resources in memory and CPU that dozens, possibly hundreds, of websites could run on a cheap VPS. That framing tells you what Joy optimises for. It is a Lisp that compiles to native code and starts fast, and a framework that avoids the object machinery of a larger stack is not going to be the memory hog. A Dockerfile is included to make the setup portable, giving you a place to mount your code and run joy without installing anything locally or managing permissions on your machine, and the repository also carries a Vagrantfile and a docker directory. The cost of that goal is visible in the release history. The versions are 0.9.0 from 2020-07-26 with the note that it is getting closer to 1.0, 0.8.0 from 2020-05-14 introducing function routes, and 0.7.4 from the same day with more bugfixes and improvements. The last push to the repository was on 2026-04-22, and the project is MIT licensed with a CHANGELOG at the top level.
Editorial conclusion
Joy fits a Janet developer who wants routing, a migration tool and a database connection without pulling in an ORM or a framework with a large dependency tree, since the whole stack is one language and the CLI generates a working project in a couple of commands. It does not fit a team that needs a maintained dependency stream, because the newest published release is 0.9.0 from 2020-07-26 and the last push to the repository was on 2026-04-22, and it does not fit a project that needs a richer data layer, because the framework states there are no ORMs, no classes and no objects. Before you build on it, read the documentation under docs/readme.md, check whether the generated project matches your expectations around routing conventions, and decide how you will handle the fact that the release line is still below 1.0.
Frequently asked questions
What is Joy and what language is it written in?
Joy is a full stack web framework written in Janet. It provides routing, request handling, a database connection layer, a migration tool and code generators, all in the same language.
How do I install Joy and create a new project?
Install Janet first, then install the joy CLI with jpm install joy. With the joy executable on your path, run joy new my-joy-project to create a project directory, then joy server to start an HTTP server listening on port 9001 locally.
How do I set up a database and a table in Joy?
Run joy create db to create an empty dev.sqlite3 file, connect it yourself in main.janet with db/connect, then run joy create table account with the column definitions to write a migration into db/migrations. Applying it with joy migrate also produces a readable db/schema.sql file.
Does Joy have an ORM?
No. The project states there are no ORMs, no classes and no objects, just functions that take requests and return responses. The database is reached through a db namespace with a connect and a disconnect, so queries are written by hand.
How current is the Joy framework?
The recent releases are 0.9.0 from 2020-07-26, 0.8.0 from 2020-05-14 and 0.7.4 from 2020-05-14, so the published line has been below 1.0 for a long time. The last push to the repository was on 2026-04-22, and there is a CHANGELOG in the repository.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/joy-framework-joy)Was this useful?