The result of taking minimalism, kaizen and TDD to an extreme
Here is a simple Minum program (see more code samples below):
public class Main {
public static void main(String[] args) {
var minum = FullSystem.initialize();
var wf = minum.getWebFramework();
wf.registerPath(GET, "",
r -> Response.htmlOk("<p>Hi there world!</p>"));
minum.block();
}
}
This web framework, "Minum", provides a full-powered minimalist foundation for a web application. For TDD, by TDD.
- Embraces the concept of "kaizen": small beneficial changes over time leading to impressive capabilities
- Has its own web server, endpoint routing, logging, templating engine, html parser, assertions framework, and database
- 100% test coverage (branch and statement) that runs in 30 seconds without any special setup (
make test_coverage
) - Nearly 100% mutation test strength using the PiTest tool. (
make mutation_test
) - Relies on no dependencies other than the Java 21 SDK - i.e. no Netty, Jetty, Tomcat, Log4j, Hibernate, MySql, etc.
- Well-documented
- No reflection
- No annotations
- No magic
- Has examples of framework use
Minum is five thousand lines of code - the "minimalist" competitors range from 400,000 to 700,000 lines when accounting for their dependencies. I have not found a similar project.
Applying a minimalist approach enables easier debugging, maintainability, and lower overall cost. Most frameworks trade faster start-up for a higher overall cost. If you need sustainable quality, the software must be well-tested and documented from the onset. As an example, this project's ability to attain such high test coverage was greatly enabled by the minimalism paradigm.
There is a 🚀 Quick start, or if you have a bit more time, consider trying the tutorial
<dependency>
<groupId>com.renomad</groupId>
<artifactId>minum</artifactId>
<version>8.0.6</version>
</dependency>
- Secure TLS 1.3 HTTP/1.1 web server
- In-memory database with disk persistence
- Server-side templating
- Logging
- Testing utilities
- HTML parsing
- Background queue processor
Compiled size: 200 kilobytes.
Lines of production code (including required dependencies)
Minum | Javalin | Spring Boot |
---|---|---|
5,340 | 141,048 | 1,085,405 |
See a size comparison in finer detail
- 19,000 http web server responses per second. details here
- 2,000,000 database updates per second. details here
- 31,717 templates rendered per second. See "test_Templating_Performance" here. Also, see this comparison benchmark, with Minum's code represented here.
See a Minum versus Spring performance comparison
See the following links for sample projects that use this framework.
This project is valuable to see the minimal-possible application that can be made. This might be a good starting point for use of Minum on a new project.
This is a good example to see a basic project with various functionality. It shows many of the typical use cases of the Minum framework.
This is a family-tree project. It demonstrates the kind of approach this framework is meant to foster.
Restaurants is a prototype project to provide a customizable ranked list of restaurants.
This is a project which uses a SQL database called H2, and which shows how a user might go about including a different database than the one built-in.
Instantiating a new database:
var db = new Db<>(foosDirectory, context, new Foo());
The Minum database keeps its data and processing primarily in memory but persists to the disk. There are pros and cons to this design choice: on the upside, it's very fast and the data stays strongly typed. On the downside, if you're not careful you could end up using a lot of memory. For certain designs, this is a suitable design constraint. On the Memoria project, the only data stored in the database is the "lean" information - user tables, sessions, primary data. Anything beyond the basics is stored in files and read from the disk as needed, with some caching to improve performance.
Obviously this won't work for all situations, and users are free to pick any other database they desire. That said, the aforementioned will work for many common situations and for prototypes, particularly if expectations are adjusted for what to store in the database.
Adding a new object to a database:
var foo = new Foo(0L, 42, "blue");
db.write(foo);
Updating an object in a database:
foo.setColor("orange");
db.write(foo);
Deleting from a database:
db.delete(foo);
Writing a log statement:
logger.logDebug(() -> "hello");
The logs are output to "standard out" during runtime. This means, if you run a Minum application from the command line, it will output its logs to the console. This is a typical pattern for servers.
The logs are all expecting their inputs as closures - the pattern is () -> "hello world"
. This keeps
the text from being processed until it needs to be. A profusion of log statements
could impact the performance of the system. By using this design pattern, those statements will only be
run if necessary, which is valuable for trace-level logging and those log statements which include
further processing (e.g. _____ has requested to _____ at _____
).
Parsing an HTML document:
List<HtmlParseNode> results = new HtmlParser().parse("<p></p>");
Minum includes a simple HTML parser. While not as fully-featured as its big brothers, it is well suited for its minimal purposes, and provides capabilities like examining returned HTML data or for use in functional tests. It is used heavily in the Memoria tests and the FamilyGraph class which handles building a graph of the family members.
Searching for an element in the parsed graph:
HtmlParseNode node;
List<HtmlParseNode> results = node.search(TagName.P, Map.of());
Creating a new web handler (a function that handles an HTTP request and returns a response):
public Response myHandler(Request r) {
return Response.htmlOk("<p>Hi world!</p>");
}
The term "web handler" refers to the bread-and-butter of what Minum provides - programs that receive HTTP requests and return HTTP responses. This example demonstrates returning an HTML message, ignoring the request data. A less contrived example would examine the "query string" or the "body" of the request for its data, and then returning an appropriate response based on that.
For example, there is sample code in Minum
which checks the authentication and returns values as HTML. There are other example endpoints in that
class, and you may see these endpoints in operation by running make run_sampledomain
from the command line, presuming you
have installed Java and GNU Make already, and then by visiting http://localhost:8080.
Registering an endpoint:
webFramework.registerPath(GET, "formentry", sd::formEntry);
The expected pattern is to have a file where all the endpoints are registered. See Memoria's endpoint registration page
Building and rendering a template:
TemplateProcessor foo = TemplateProcessor.buildProcessor("hello {{ name }}");
String rendered = foo.renderTemplate(Map.of("name", "world"));
The Minum framework is driven by a paradigm of server-rendered HTML. Is is performant and works on all devices. In contrast, the industry's current predominant approach is single page apps, whose overall system complexity is greater. Complexity is a dragon we must fight daily.
The templates can be any string, but the design was driven concerned with rendering HTML templates. Here is an example of a simple template, which is rendered with dynamic data in this class
It is a common pattern to get user data from requests by query string or body. The following examples show this:
Getting a query parameter from a request:
String id = r.requestLine().queryString().get("id");
Getting a body parameter from a request, as a string:
String personId = request.body().asString("person_id");
Get a path parameter from a request as a string:
Pattern requestRegex = Pattern.compile(".well-known/acme-challenge/(?<challengeValue>.*$)");
final var challengeMatcher = requestRegex.matcher(request.requestLine().getPathDetails().isolatedPath());
// When the find command is run, it changes state so we can search by matching group
if (! challengeMatcher.find()) {
return new Response(StatusLine.StatusCode.CODE_400_BAD_REQUEST);
}
String tokenFileName = challengeMatcher.group("challengeValue");
This more complicated scenario shows handling a request from the LetsEncrypt ACME challenge for renewing certificates. Because the incoming request comes as a "path parameter", we have to extract the data using a regular expression.
In this example, if we don't find a match, we return a 400 error HTTP status code, and otherwise get the data by a named matching group in our regular expression.
To register an endpoint that allows "path parameters", we register a partial path, like the following, which will match if the provided string is contained anywhere in an incoming URL. There are some complications to matching this way, so it is recommended to use this approach as little as possible. In the Memoria project, this is only used for LetsEncrypt, which requires it. All other endpoints get their user data from query strings, headers, and bodies.
webFramework.registerPartialPath(GET, ".well-known/acme-challenge", letsEncrypt::challengeResponse);
Getting a body parameter from a request, as a byte array:
byte[] photoBytes = body.asBytes("image_uploads");
The photo bytes example is seen in the UploadPhoto class
Checking for a log message during tests:
assertTrue(logger.doesMessageExist("Bad path requested at readFile: ../testingreadfile.txt"));
The Minum application was built using test-driven development (TDD) from the ground up. The testing mindset affected every aspect of its construction. One element that can sometimes trip up developers is when they are testing that something happened elsewhere in the system as a result of an action. If that separate action has logging, then a test can examine the logs for a correct output.