1. Overview

Blade is a tiny Java 8+ MVC framework, built from scratch with some clear goals in mind: to be self-contained, productive, elegant, intuitive, and super fast.

Many different frameworks inspired its design: Node’s Express, Python’s Flask, and Golang’s Macaron / Martini.

Blade is also part of an ambitiously larger project, Let’s Blade. It includes a heterogeneous collection of other small libraries, from Captcha generation to JSON conversion, from templating to a simple database connection.

However, in this tutorial, we’ll focus on the MVC only.

2. Getting Started

First of all, let’s create an empty Maven project and add the latest Blade MVC dependency in the pom.xml:


2.1. Bundling a Blade Application

Since our app will be created as a JAR, it won’t have a /lib folder, like in a WAR. As a result, this leads us to the problem how to provide the blade-mvc JAR, along with any other JAR we might need, to our app.

The different ways of doing this, each one with pros and cons, are explained in the How to Create an Executable JAR with Maven tutorial.

For simplicity, we’ll use the Maven Assembly Plugin technique, which explodes any JAR imported in the pom.xml and subsequently bundles all the classes together in a single uber-JAR.

2.2. Running a Blade Application

Blade is based upon Netty, an amazing asynchronous event-driven network application framework. Therefore, to run our Blade-based application we don’t need any external Application Server nor Servlet Container; the JRE will be enough:

java -jar target/sample-blade-app.jar

After that, the application will be accessible at the http://localhost:9000 URL.

3. Understanding the Architecture

The architecture of Blade is very straightforward:


It always follows the same life cycle:

  1. Netty receives a request
  2. Middlewares are executed (optional)
  3. WebHooks are executed (optional)
  4. Routing is performed
  5. The response is sent to the client
  6. Cleanup

We’ll explore the above functions in the next sections.

4. Routing

In short, routing in MVC is the mechanism used to create a binding between an URL and a Controller.

Blade provides two types of routes: a basic one and an annotated one.

4.1. Basic Routes

Basic routes are intended for very small software, like microservices or minimal web applications:

  .get("/basic-routes-example", ctx -> ctx.text("GET called"))
  .post("/basic-routes-example", ctx -> ctx.text("POST called"))
  .put("/basic-routes-example", ctx -> ctx.text("PUT called"))
  .delete("/basic-routes-example", ctx -> ctx.text("DELETE called"))
  .start(App.class, args);

The name of the method used to register a route corresponds to the HTTP verb that will be used to forward the request. As simple as that.

In this case, we’re returning a text, but we can also render pages, as we’ll see later in this tutorial.

4.2. Annotated Routes

Certainly, for more realistic use cases we can define all the routes we need using annotations. We should use separate classes for that.

First of all, we need to create a Controller through the @Path annotation, which will be scanned by Blade during the startup.

We then need to use the route annotation related to the HTTP method we want to intercept:

public class RouteExampleController {    
    public String get(){ 
        return "get.html"; 
    public String post(){ 
        return "post.html"; 
    public String put(){ 
        return "put.html"; 
    public String delete(){ 
        return "delete.html"; 

We can also use the simple @Route annotation and specify the HTTP method as a parameter:

@Route(value="/another-route-example", method=HttpMethod.GET) 
public String anotherGet(){ 
    return "get.html" ; 

On the other hand, if we don’t put any method parameter, the route will intercept every HTTP call to that URL, no matter the verb.

4.3. Parameter Injection

There are several ways to pass parameters to our routes. Let’s explore them with some examples from the documentation.

  • Form parameter:
public void formParam(@Param String name){
    System.out.println("name: " + name);
  • Restful parameter:
public void restfulParam(@PathParam Integer uid){
    System.out.println("uid: " + uid);
  • File upload parameter:
public void fileParam(@MultipartParam FileItem fileItem){
    byte[] file = fileItem.getData();
  • Header parameter:
public void headerParam(@HeaderParam String referer){
    System.out.println("Referer: " + referer);
  • Cookie parameter:
public void cookieParam(@CookieParam String myCookie){
    System.out.println("myCookie: " + myCookie);
  • Body parameter:
public void bodyParam(@BodyParam User user){
    System.out.println("user: " + user.toString());
  • Value Object parameter, called by sending its attributes to the route:
public void voParam(@Param User user){
    System.out.println("user: " + user.toString());
<form method="post">
    <input type="text" name="age"/>
    <input type="text" name="name"/>

5. Static Resources

Blade can also serve static resources if needed, by simply putting them inside the /resources/static folder.

For example, the src/main/resources/static/app.css will be available at http://localhost:9000/static/app.css.

5.1. Customizing the Paths

We can tune this behavior by adding one or more static paths programmatically:


The same result is obtainable through configuration, by editing the file src/main/resources/application.properties:


5.2. Enabling the Resources Listing

We can allow the listing of a static folder’s content, a feature turned off by default for a security reason:


Or in the configuration:


We can now open the http://localhost:9000/custom-static/ to show the content of the folder.

5.3. Using WebJars

As seen in the Introduction to WebJars tutorial, static resources packaged as JAR are also a viable option.

Blade exposes them automatically under the /webjars/ path.

For instance, let’s import Bootstrap in the pom.xml:


As a result, it’ll be available under http://localhost:9000/webjars/bootstrap/4.2.1/css/bootstrap.css

6. HTTP Request

Since Blade is not based on the Servlet Specification, objects like its interface Request and its class HttpRequest/em> are slightly different than the ones we’re used to.

6.1. Form Parameters

When reading form parameters, Blade makes great use of Java’s Optional in the results of the query methods (all methods below return an Optional object):

  • query(String name)
  • queryInt(String name)
  • queryLong(String name)
  • queryDouble(String name)

They’re also available with a fallback value:

  • String query(String name, String defaultValue)
  • int queryInt(String name, int defaultValue)
  • long queryLong(String name, long defaultValue)
  • double queryDouble(String name, double defaultValue)

We can read a form parameter through the automapped property:

public void formParams(@Param String username){
    // ...

Or from the Request object:

public void formParams(Request request){
    String username = request.query("username", "Baeldung");

6.2. JSON Data

Let’s now take a look at how a JSON object can be mapped to a POJO:

curl -X POST http://localhost:9000/users -H 'Content-Type: application/json' \ 
  -d '{"name":"Baeldung","site":"baeldung.com"}'

POJO (annotated with Lombok for readability):

public class User {
    @Getter @Setter private String name;
    @Getter @Setter private String site;

Again, the value is available as the injected property:

public void bodyParams(@BodyParam User user){
    // ...

And from the Request:

public void bodyParams(Request request) {
    String bodyString = request.bodyToString();

6.3. RESTful Parameters

RESTFul parameters in pretty URLs like localhost:9000/user/42 are also first-class citizens:

public void user(@PathParam Integer id){
    // ...

As usual, we can rely on the Request object when needed:

public void user(Request request){
    Integer id = request.pathInt("id");

Obviously, the same method is available for Long and String types too.

6.4. Data Binding

Blade supports both JSON and Form binding parameters and attaches them to the model object automatically:

public void bodyParams(User user){}

6.5. Request and Session Attributes

The API for reading and writing objects in a Request and a Session are crystal clear.

The methods with two parameters, representing key and value, are the mutators we can use to store our values in the different contexts:

Session session = request.session();
request.attribute("request-val", "Some Request value");
session.attribute("session-val", 1337);

On the other hand, the same methods accepting only the key parameter are the accessors:

String requestVal = request.attribute("request-val");
String sessionVal = session.attribute("session-val"); //It's an Integer

An interesting feature is their Generic return type <T> T, which saves us from the need of casting the result.

6.6. Headers

Request headers, on the contrary, can only be read from the request:

String header1 = request.header("a-header");
String header2 = request.header("a-safe-header", "with a default value");
Map<String, String> allHeaders = request.headers();

6.7. Utilities

The following utility methods are also available out of the box, and they’re so evident that don’t need further explanations:

  • boolean isIE()
  • boolean isAjax()
  • String contentType()
  • String userAgent()

6.8. Reading Cookies

Let’s see how the Request object helps us deal with Cookies, specifically when reading the Optional<Cookie>:

Optional<Cookie> cookieRaw(String name);

We can also get it as a String by specifying a default value to apply if a Cookie doesn’t exist:

String cookie(String name, String defaultValue);

Finally, this is how we can read all the Cookies at once (keys are Cookies’ names, values are Cookies’ values):

Map<String, String> cookies = request.cookies();

7. HTTP Response

Analogous to what was done with the Request, we can obtain a reference to the Response object by simply declaring it as a parameter of the routing method:

public void home(Response response) {}

7.1. Simple Output

We can easily send a simple output to the caller through one of the handy output methods, along with a 200 HTTP code and the appropriate Content-Type.

Firstly, we can send a plain text:

response.text("Hello World!");

Secondly, we can produce an HTML:

response.html("<h1>Hello World!</h1>");

Thirdly, we can likewise generate an XML:

response.xml("<Msg>Hello World!</Msg>");

Finally, we can output JSON using a String:

response.json("{\"The Answer\":42}");

And even from a POJO, exploiting the automatic JSON conversion:

User user = new User("Baeldung", "baeldung.com"); 

7.2. File Output

Downloading a file from the server couldn’t be leaner:

response.download("the-file.txt", "/path/to/the/file.txt");

The first parameter sets the name of the file that will be downloaded, while the second one (a File object, here constructed with a String) represents the path to the actual file on the server.

7.3. Template Rendering

Blade can also render pages through a template engine:


The templates default directory is src/main/resources/templates/, hence the previous one-liner will look for the file src/main/resources/templates/admin/users.html.

We’ll learn more about this later, in the Templating section.

7.4. Redirect

A redirection means sending a 302 HTTP code to the browser, along with an URL to follow with a second GET.

We can redirect to another route, or to an external URL as well:


7.5. Writing Cookies

We should be used to the simplicity of Blade at this point. Let’s thus see how we can write an unexpiring Cookie in a single line of code:

response.cookie("cookie-name", "Some value here");

Indeed, removing a Cookie is equally simple:


7.6. Other Operations

Finally, the Response object provides us with several other methods to perform operations like writing Headers, setting the Content-Type, setting the Status code, and so on.

Let’s take a quick look at some of them:

  • Response status(int status)
  • Map headers()
  • Response notFound()
  • Map cookies()
  • Response contentType(String contentType)
  • void body(@NonNull byte[] data)
  • Response header(String name, String value)

8. WebHooks

A WebHook is an interceptor through which we can run code before and after the execution of a routing method.

We can create a WebHook by simply implementing the WebHook functional interface and overriding the before() method:

public interface WebHook {

    boolean before(RouteContext ctx);

    default boolean after(RouteContext ctx) {
        return true;

As we can see, after() is a default method, hence we’ll override it only when needed.

8.1. Intercepting Every Request

The @Bean annotation tells the framework to scan the class with the IoC Container.

A WebHook annotated with it will consequently work globally, intercepting requests to every URL:

public class BaeldungHook implements WebHook {

    public boolean before(RouteContext ctx) {
        System.out.println("[BaeldungHook] called before Route method");
        return true;

8.2. Narrowing to a URL

We can also intercept specific URLs, to execute code around those route methods only:

  .before("/user/*", ctx -> System.out.println("Before: " + ctx.uri()));
  .start(App.class, args);

8.3. Middlewares

Middlewares are prioritized WebHooks, which get executed before any standard WebHook:

public class BaeldungMiddleware implements WebHook {

    public boolean before(RouteContext context) {
        System.out.println("[BaeldungMiddleware] called before Route method and other WebHooks");
        return true;

They simply need to be defined without the @Bean annotation, and then registered declaratively through use():

  .use(new BaeldungMiddleware())
  .start(App.class, args);

In addition, Blade comes with the following security-related built-in Middlewares, whose names should be self-explanatory:

  • BasicAuthMiddleware
  • CorsMiddleware
  • XssMiddleware
  • CsrfMiddleware

9. Configuration

In Blade, the configuration is totally optional, because everything works out-of-the-box by convention. However, we can customize the default settings, and introduce new attributes, inside the src/main/resources/application.properties file.

9.1. Reading the Configuration

We can read the configuration in different ways, with or without specifying a default value in case the setting is not available.

  • During startup:
  .on(EventType.SERVER_STARTED, e -> {
      Optional<String> version = WebContext.blade().env("app.version");
  .start(App.class, args);
  • Inside a route:
public void someRoute(){
    String authors = WebContext.blade().env("app.authors","Unknown authors");
  • In a custom loader, by implementing the BladeLoader interface, overriding the load() method, and annotating the class with @Bean:
public class LoadConfig implements BladeLoader {

    public void load(Blade blade) {
        Optional<String> version = WebContext.blade().env("app.version");
        String authors = WebContext.blade().env("app.authors","Unknown authors");

9.2. Configuration Attributes

The several settings already configured, but ready to be customized, are grouped by type and listed at this address in three-column tables (name, description, default value). We can also refer to the translated page, paying attention to the fact that the translation erroneously capitalizes the settings’ names. The real settings are fully lowercase.

Grouping configuration settings by prefix makes them readable all at once into a map, which is useful when there are many of them:

Environment environment = blade.environment();
Map<String, Object> map = environment.getPrefix("app");
String version = map.get("version").toString();
String authors = map.get("authors","Unknown authors").toString();

9.3. Handling Multiple Environments

When deploying our app to a different environment, we might need to specify different settings, for example the ones related to the database connection. Instead of manually replacing the application.properties file, Blade offers us a way to configure the app for different environments. We can simply keep application.properties with all the development settings, and then create other files in the same folder, like application-prod.properties, containing only the settings that differ.

During the startup, we can then specify the environment we want to use, and the framework will merge the files by using the most specific settings from application-prod.properties, and all the other settings from the default application.properties file:

java -jar target/sample-blade-app.jar --app.env=prod

10. Templating

Templating in Blade is a modular aspect. While it integrates a very basic template engine, for any professional use of the Views we should rely on an external template engine. We can then choose an engine from the ones available in the blade-template-engines repository on GitHub, which are FreeMarker, Jetbrick, Pebble, and Velocity, or even creating a wrapper to import another template we like.

Blade’s author suggests Jetbrick, another smart Chinese project.

10.1. Using the Default Engine

The default template works by parsing variables from different contexts through the ${} notation:

<h1>Hello, ${name}!</h1>

10.2. Plugging in an External Engine

Switching to a different template engine is a breeze! We simply import the dependency of (the Blade wrapper of) the engine:


At this point, it’s enough to write a simple configuration to instruct the framework to use that library:

public class TemplateConfig implements BladeLoader {

    public void load(Blade blade) {
        blade.templateEngine(new JetbrickTemplateEngine());

As a result, now every file under src/main/resources/templates/ will be parsed with the new engine, whose syntax is beyond the scope of this tutorial.

10.3. Wrapping a New Engine

Wrapping a new template engine requires creating a single class, which must implement the TemplateEngine interface and override the render() method:

void render (ModelAndView modelAndView, Writer writer) throws TemplateException;

For this purpose, we can take a look at the code of the actual Jetbrick wrapper to get an idea of what that means.

11. Logging

Blade uses slf4j-api as logging interface.

It also includes an already configured logging implementation, called blade-log. Therefore, we don’t need to import anything; it works as is, by simply defining a Logger:

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);

11.1. Customizing the Integrated Logger

In case we want to modify the default configuration, we need to tune the following parameters as System Properties:

  • Logging levels (can be “trace”, “debug”, “info”, “warn”, or “error”):
# Root Logger

# Package Custom Logging Level

# Class Custom Logging Level
  • Displayed information:
# Date and Time

# Date and Time Pattern
com.blade.logger.datePattern=yyyy-MM-dd HH:mm:ss:SSS Z

# Thread Name

# Logger Instance Name

# Only the Last Part of FQCN
  • Logger:
# Path 

# Name (it defaults to the current app.name)

11.2. Excluding the Integrated Logger

Although having an integrated logger already configured is very handy to start our small project, we might easily end up in the case where other libraries import their own logging implementation. And, in that case, we’re able to remove the integrated one in order to avoid conflicts:


12. Customizations

12.1. Custom Exception Handling

An Exception Handler is also built-in by default in the framework. It prints the exception to the console, and if app.devMode is true, the stack trace is also visible on the webpage.

However, we can handle an Exception in a specific way by defining a @Bean extending the DefaultExceptionHandler class:

public class GlobalExceptionHandler extends DefaultExceptionHandler {

    public void handle(Exception e) {
        if (e instanceof BaeldungException) {
            BaeldungException baeldungException = (BaeldungException) e;
            String msg = baeldungException.getMessage();
        } else {

12.2. Custom Error Pages

Similarly, the errors 404 – Not Found and 500 – Internal Server Error are handled through skinny default pages.

We can force the framework to use our own pages by declaring them in the application.properties file with the following settings:


Certainly, those HTML pages must be placed under the src/main/resources/templates folder.

Within the 500 one, we can moreover retrieve the exception message and the stackTrace through their special variables:

<!DOCTYPE html>
        <meta charset="utf-8">
        <title>500 Internal Server Error</title>
        <h1> Custom Error 500 Page </h1>
        <p> The following error occurred: "<strong>${message}</strong>"</p>
        <pre> ${stackTrace} </pre>

13. Scheduled Tasks

Another interesting feature of the framework is the possibility of scheduling the execution of a method.

That’s possible by annotating the method of a @Bean class with the @Schedule annotation:

public class ScheduleExample {

    @Schedule(name = "baeldungTask", cron = "0 */1 * * * ?")
    public void runScheduledTask() {
        System.out.println("This is a scheduled Task running once per minute.");

Indeed, it uses the classical cron expressions to specify the DateTime coordinates. We can read more about those in A Guide to Cron Expressions.

Later on, we might exploit the static methods of the TaskManager class to perform operations on the scheduled tasks.

  • Get all the scheduled tasks:
List<Task> allScheduledTasks = TaskManager.getTasks();
  • Get a task by name:
Task myTask = TaskManager.getTask("baeldungTask");
  • Stop a task by name:
boolean closed = TaskManager.stopTask("baeldungTask");

14. Events

As already seen in section 9.1, it’s possible to listen for a specified event before running some custom code.

Blade provides the following events out of the box:

public enum EventType {

While the first six are easy to guess, the last two need some hints: ENVIRONMENT_CHANGED allows us to perform an action if a configuration file changes when the server is up. SOURCE_CHANGED, instead, is not yet implemented and is there for future use only.

Let’s see how we can put a value in the session whenever it’s created:

  .on(EventType.SESSION_CREATED, e -> {
      Session session = (Session) e.attribute("session");
      session.attribute("name", "Baeldung");
  .start(App.class, args);

15. Session Implementation

Talking about the session, its default implementation stores session values in-memory.

We might, thus, want to switch to a different implementation to provide cache, persistence, or something else. Let’s take Redis, for example. We’d first need to create our RedisSession wrapper by implementing the Session interface, as shown in the docs for the HttpSession.

Then, it’d be only a matter of letting the framework know we want to use it. We can do this in the same way we did for the custom template engine, with the only difference being that we call the sessionType() method:

public class SessionConfig implements BladeLoader {
    public void load(Blade blade) {
        blade.sessionType(new RedisSession());

16. Command Line Arguments

When running Blade from the command line, there are three settings we can specify to alter its behavior.

Firstly, we can change the IP address, which by default is the local loopback:

java -jar target/sample-blade-app.jar --server.address=

Secondly, we can also change the port, which by default is 9000:

java -jar target/sample-blade-app.jar --server.port=8080

Finally, as seen in section 9.3, we can change the environment to let a different application-XXX.properties file to be read over the default one, which is application.properties:

java -jar target/sample-blade-app.jar --app.env=prod

17. Running in the IDE

Any modern Java IDE is able to play a Blade project without even needing the Maven plugins. Running Blade in an IDE is especially useful when running the Blade Demos, examples written expressly to showcase the framework’s functionalities. They all inherit a parent pom, so it’s easier to let the IDE do the work, instead of manually tweaking them to be run as standalone apps.

17.1. Eclipse

In Eclipse, it’s enough to right-click on the project and launch Run as Java Application, select our App class, and press OK.

Eclipse’s console, however, will not display ANSI colors correctly, pouring out their codes instead:

eclipse workspace sample app - Eclipse IDE

Luckily, installing the ANSI Escape in Console extension fixes the problem for good:

eclipse workspace - Eclipse IDE

17.2. IntelliJ IDEA

IntelliJ IDEA works with ANSI colors out of the box. Therefore, it’s enough to create the project, right-click on the App file, and launch Run ‘App.main()’ (which is equivalent to pressing Ctrl+Shift+F10):

sample blade app

17.3. Visual Studio Code

It’s also possible to use VSCode, a popular non-Java-centric IDE, by previously installing the Java Extension Pack.

Pressing Ctrl+F5 will then run the project:

pom.xml sample app

18. Conclusion

We’ve seen how to use Blade to create a small MVC application.

The entire documentation is available only in the Chinese language. Despite being widespread mainly in China, thanks to its Chinese origins, the author has recently translated the API and documented the core functionalities of the project in English on GitHub.

As always, we can find the source code of the example over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

res – REST with Spring (eBook) (everywhere)
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.