/minum

A minimalist Java web framework built from scratch

Primary LanguageJavaMIT LicenseMIT

Minum Web Framework

When you need the fewest moving parts

The simplest 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();
    }
}

What is this?

This web framework, "Minum", provides a full-powered minimalist foundation for a web application. For TDD, by TDD.

  • Has its own web server, endpoint routing, logging, templating engine, html parser, assertions framework, and database
  • Around 100% test coverage 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
  • Uses no reflection
  • Requires no annotations
  • No magic
  • Has examples of framework use:
    • a tiny project, as the basis to get started
    • a small project, showing some minimal use cases
    • a full application demonstrating realistic usage

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.

Getting Started

There is a 🚀 Quick start, or if you have a bit more time, consider trying the tutorial

Maven

<dependency>
    <groupId>com.renomad</groupId>
    <artifactId>minum</artifactId>
    <version>8.0.2</version>
</dependency>

Features:

Size Comparison:

Compiled size: 200 kilobytes.

Lines of production code (including required dependencies)

Minum Javalin Spring Boot
5,262 141,048 1,085,405

See details

Performance:

  • 19,000 http web server responses per second. detail
  • 2,000,000 database updates per second. detail
  • 31,717 templates rendered per second. See "test_Templating_Performance" here. Also, see this comparison benchmark, with Minum's code represented here.

See framework performance comparison

Documentation:

Example projects demonstrating usage:

See the following links for sample projects that use this framework.

Smallest-possible

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.


Example

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.


Memoria project

This is a family-tree project. It demonstrates the kind of approach this framework is meant to foster.

Code samples

Instantiating a new database:

var db = new Db<>(foosDirectory, context, new Foo());

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");

Parsing an HTML document:

List<HtmlParseNode> results = new HtmlParser().parse("<p></p>");

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>");
}

Registering that endpoint:

webFramework.registerPath(GET, "formentry", sd::formEntry);

Building and rendering a template:

TemplateProcessor foo = TemplateProcessor.buildProcessor("hello {{ name }}");
String rendered = foo.renderTemplate(Map.of("name", "world"));

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");

Getting a body parameter from a request, as a byte array:

byte[] photoBytes = body.asBytes("image_uploads");

Checking for a log message during tests:

assertTrue(logger.doesMessageExist("Bad path requested at readFile: ../testingreadfile.txt"));