Back To The Future: Server-Side Web Pages With Kotlin (pt. 1)

Apiumhub - Apr 19 '23 - - Dev Community

Introduction: Server-Side Web Pages With Kotlin

Web development has undergone a variety of changes since the internet became popularized in the 1990s:

  • First came the most basic of the basic: HTML pages that were completely statically rendered, with no dynamism whatsoever.
  • Later came technologies like the Common Gateway Interface that allowed for generating HTML code for a webpage programmatically.
  • Then came templating engines like JavaServer Pages (now Jakarta Server Pages), ASP.NET, and Thymeleaf that enabled developers to work with template files that were predominantly “HTML-looking” with programming code intermixed.
  • Next came Javascript-based “client-side scripting” frameworks like Angular, React, and Vue which transformed web development into two separate disciplines: the “back-end” development that contained the traditional web server and business logic code along with “front-end” development (using the aforementioned frameworks) that would be concerned with a website’s visualization and receive data from the backend.

However, this is not to say that development trends only advance in one direction and never backward. For example, NoSQL databases like MongoDB quickly gained popularity in no small part due to their ability to hold unstructured data compared to traditional SQL databases like PostgreSQL and MySQL, yet those latter databases have evolved as well and can now contain unstructured data via the JSONB and JSON data types, respectively. Likewise, new Javascript frameworks like Next.js are starting to offer options for server-side rendering alongside their now-traditional client-side rendering capabilities. Likewise, server-side templating engines like Thymeleaf have also continued to evolve, with Thymeleaf releasing a new version of the framework last December.

But why continue using server-side rendering? Admitting as much – especially for a new project – might draw reactions of disdain in the world of web development where the question asked is not whether one uses client-side frameworks like Angular, React, or Vue, but rather which of those client-side frameworks is being used. Despite this, server-side web page generation does pose some advantages compared to client-side web page generation:

  • More lightweight experience for the end-user: instead of all of the Javascript libraries that are needed to run a client-side application (along with the moving parts that are needed to run the said application), all that is delivered to a user’s browser is the HTML code that the server has generated along with any auxiliary Javascript and CSS files.
  • Server-side web pages can leverage more security-related processing by virtue of the code that renders the web pages having direct access to the security functionality of the server.
  • Search Engine Optimization (SEO) – while not being impossible to implement with client-side rendering – is easier to conduct with server-side rendering.

In addition, there still remain active options for “going further back in time”, i.e. using server-side templating engines. For example, Thymeleaf – one of the most well-known choices for Java-based server-side templating engines- also continues to evolve, with Thymeleaf releasing a new version of its framework last December and was highlighted at the Spring I/O 2022 conference in Barcelona. Again asking the question: why would one choose this instead of the previous options? Alongside the advantages listed above for server-side rendering compared to client-side rendering, templating engines offer further benefits:

  • There will be less potential compatibility issues with regard to whether the user’s browser is capable of executing the Javascript functionality necessary to make the requested web page render and carry out its duties correctly.
  • Full-stack development is easier due to there essentially being no separation between the “back-end” and “front-end” code, moreover there being no separate tech stack for the web page generation functionality means less of a learning curve for the project.

With that in mind, what options are available for a developer who wants to create a Spring Boot web application with purely Java-based server-side rendering? There are quite a few choices for templating engines alongside Thymeleaf that one could leverage. In addition to this variety of options, though, another option is available within the realm of the Kotlin ecosystem. The developers of the Kotlin programming language have been busy at work not just with the language itself, but also with a variety of supporting libraries that leverage the language’s capabilities to provide new tools for developers. One of these libraries is the kotlinx.html library which – as the library’s name suggests – allows a developer to generate HTML in a “Kotlin-esque” manner.

About kotlinx.html

At its core, the kotlinx.html creates essentially a “Domain-Specific Language” (DSL) for writing code that generates or manipulates HTML code. To give an example, here is a small program that launches a Node.JS server and appends HTML code to the resulting webpage body:

fun main() {
   window.onload = { document.body?.sayHello() }
}

fun Node.sayHello() {
   append {
       div {
           div {
               onClickFunction = { event ->
                   window.alert("I was clicked - ${event.timeStamp}")
               }
               p {
                   +"This is a paragraph"
               }
           }
           a(href = "https://github.com/kotlin/kotlinx.html") {
               +"This is a link"
           }
       }
   }
}

Enter fullscreen mode Exit fullscreen mode

This results in the following webpage:

D3 SMMij7ZDmwjK5pcX4zgfdEBgi2HxplexcXpPeB UbxlcE oAVccoGLoje OM3S1kdhQK4DIznd0UWjkkKKDzWcgb0T4TKYKIv5Z39EnInDAb9pYqnQ toOy4bfQAP7468KrSGiCrEXinaDJh4Vbk

Very basic, but as we’ll see later, more “realistic” HTML code can be generated via this library. The above example still provides examples of the key parts of Kotlin functionality that provide the capability to write this DSL:

  • Extension Functions: It is possible to create a “new” member function for any class in Kotlin, including pre-existing classes like String or the Node class in the example code (which is a basic translation of the Nodeclass in the Web DOM model). In essence, this is syntactic sugar for defining a function that accepts an instance of the target class as the first parameter of the function along with the remaining defined parameters, but the benefit is that it allows for cleaner-looking function calls.
  • Scope Functions: These are functions that accept a lambda expression that take the object on which the function has been called as the “receiver” – either implicit via this keyword or explicitly – of any function calls within the lambda. As the code demonstrates, this allows the developer to “embed” HTML tags within one another in a way that is similar to actual HTML code.
  • Named and Default Function Arguments: When declaring HTML tags within the Kotlin code, it is possible to pass in arguments to the specific tag attributes that need configuration. The declaration of the hyperlink tag demonstrates this by specifying thehrefattribute while leaving the remaining arguments of the tag declaration – target and classes – to be filled in by default according to the function definition.
  • Operator Overloading: Kotlin permits the definition of functions that use traditional operators like +, -, *, /, and so on. As with extension functions, this is syntactic sugar for functions that take in the arguments involved in the declaration within the source code. In the case of the code above, it serves to specify any text value that is to be placed within the enclosing HTML tag (note that the function text()is also available, and the operator function + here serves as a pass-through to text() as well).

Put together, these features highlight how powerful Kotlin’s capability is to create DSLs for various purposes. Those who would like to see another example of this functionality in action are encouraged to take a look at the Kotlin DSL for Gradle, an alternative to the traditional Groovy-based markup language for Gradle that has been in production since the release of Gradle 5.0 in 2018.

Experimenting

So how does the kotlinx.html library compare to a traditional templating engine like Thymeleaf? The most natural way to demonstrate this is to attempt to build the same Spring Boot-powered website – a rudimentary website with Bootstrap styling for a hypothetical bookstore that allows the user to view, add, and remove books and their authors – using the two approaches: one that employs Thymeleaf, and one that leverages kotlinx.html.

Thymeleaf

The approach for Thymeleaf is relatively straightforward:

  • First, add org.springframework.boot:spring-boot-starter-thymeleaf to the list of project dependencies (in this case version 3.0.2).

Then, create a template HTML file in the resources/templates directory that contains the necessary Thymeleaf code for the desired webpage:


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
   <title>Bookstore - View Authors</title>
   <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
   <script th:src="@{/js/bootstrap.min.js}"></script>
   <script th:inline="javascript">
       function confirmDelete(name) {
           return window.confirm("Really delete author " + name + "?");
       }
   </script>
</head>
<body>
<div th:insert="~{fragments/general.html :: header(pageName = 'Authors')}"></div>
<div id="content">
   <h2>Our Books' Authors</h2>
   <ul>
       <li th:each="author : ${authors}">
           <form method="post" th:action="@{/authors/{authorId}/delete(authorId=${author.id})}"
                 style="margin-block-end: 1em;" th:onsubmit="return confirmDelete([[${author.name}]])">
               <a th:href="@{/authors/{authorId}(authorId=${author.id})}" th:text="${author.name}"></a>
               <button type="submit" class="btn btn-danger">Delete</button>
           </form>
       </li>
   </ul>
   <a class="btn btn-primary" th:href="@{/authors/add}">Add Author</a>
</div>
<div th:insert="~{fragments/general.html :: footer}"></div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

This file is for viewing all authors whose books the bookstore has available and is called view_authors.html.

  • Any fragments of Thymeleaf/HTML code that one may wish to reuse – like the header and footer fragments that are used in each webpage – can be placed in a separate HTML template file, in this case resources/templates/fragments/general.html:

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head th:fragment="headerfiles">
   <meta charset="UTF-8"/>
   <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
   <script th:src="@{/js/bootstrap.min.js}"></script>
   <title></title>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" th:fragment="header (pageName)">
   <div class="container-fluid">
       <a class="navbar-brand" href="/">Test Bookstore</a>
       <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarHeader">
           <span class="navbar-toggler-icon"></span>
       </button>

       <div class="collapse navbar-collapse" id="navbarHeader">
           <ul class="navbar-nav me-auto mb-2 mb-lg-0">
               <li class="nav-item">
                   <a th:if="${pageName} == 'Home'" class="nav-link active" href="/">Home</a>
                   <a th:unless="${pageName} == 'Home'" class="nav-link" href="/">Home</a>
               </li>
               <li class="nav-item">
                   <a th:if="${pageName} == 'Authors'" class="nav-link active" href="/authors">Authors</a>
                   <a th:unless="${pageName} == 'Authors'" class="nav-link" href="/authors">Authors</a>
               </li>
               <li class="nav-item">
                   <a th:if="${pageName} == 'Books'" class="nav-link active" href="/books">Books</a>
                   <a th:unless="${pageName} == 'Books'" class="nav-link" href="/books">Books</a>
               </li>
           </ul>
       </div>
   </div>
</nav>
<footer th:fragment="footer" class="footer mt-auto py-3 bg-light fixed-bottom">
       <p class="text-center">Copyright 20XX Bookstore Productions - All Rights Reserved</p>
</footer>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode
  • Next, create a web controller where the endpoints return a string value that corresponds to the name of the HTML file (minus the .html file extension) that was created in the step above:

@Controller
@RequestMapping("/authors")
class AuthorController(private val authorService: AuthorService) {
   @GetMapping
   fun getAll(model: Model): String {
       model["authors"] = authorService.getAll()
       return "view_authors"
   }

   @GetMapping("/{id}")
   fun get(@PathVariable id: Int, model: Model): String {
       model["author"] = authorService.get(id)
       return "view_author"
   }

   @GetMapping("/add")
   fun add(model: Model): String {
       model["authorForm"] = AuthorForm()
       return "add_author"
   }

   @PostMapping("/save")
   fun save(@Valid authorForm: AuthorForm, bindingResult: BindingResult): String {
       return if (!bindingResult.hasErrors()) {
           authorService.save(authorForm)
           "redirect:/authors"
       } else {
           "add_author"
       }
   }

   @PostMapping("/{id}/delete")
   fun delete(@PathVariable id: Int): String {
       authorService.delete(id)
       return "redirect:/authors"
   }
}

Enter fullscreen mode Exit fullscreen mode

The first endpoint – the root /authors request path in the controller – instructs the Thymeleaf templating engine to generate a webpage according to the view_authors.html template file; the authors variable invoked in the template file is supplied via the model object that is passed into the controller function. Note that redirects are done by having the controller method return the destination endpoint (*not* the Thymeleaf template file!) prefixed with redirect: as can be seen in the endpoint to delete an author.

  • In order to create a catch-all webpage for handling error responses, it is necessary to create a template page called error.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
   <title>Bookstore - Error</title>
   <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
   <script th:src="@{/js/bootstrap.min.js}"></script>
</head>
<body>
<div th:insert="~{fragments/general.html :: header(pageName = 'Error')}"></div>
<h2>Oops!</h2>
<p th:text="'An error occurred and provided the status ' + ${status}"></p>
<div th:insert="~{fragments/general.html :: footer}"></div>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode
  • Conducting requests against a warmed-up instance of this website produces an average load time of ~43ms and 218 kilobytes of bandwidth used (mostly for the Bootstrap Javascript and CSS files):

bE4AYuxV3TiMQPczu0GGH7HYHHEvOlEyIF5SuadU2D45t 4OZHcsP6kZm 6Q LhD8uNiqe2boyb7TO8weOQMuZWmkjy yt71prLzyWV52Gqd7ZDmWO6APomDEdIzxXSgtMYB11wDoGM0D0pM39X 9kc

With these steps, one can create a fully-functioning website; those who wish for more information about more advanced features in Thymeleaf can refer to its website here.

kotlinx.html

Creating an equivalent web application using kotlinx.html requires a different workflow: as the kotlinx.html library generates HTML code directly and does not work with HTML template files, all HTML-related code can be placed directly within the source directory.

  • First, add org.jetbrains.kotlinx:kotlinx-htmlto the list of project dependencies (in this case version 0.8.0).
  • Then, create a Kotlin class within the class structure that will render the code for the desired webpage:

@Service
class ViewAuthorsPageRenderer(private val authorService: AuthorService) {
   fun renderPage(): String {
       val authors = authorService.getAll()
       return writePage {
           head {
               title("Bookstore - View Authors")
               link(href = "/css/bootstrap.min.css", rel = "stylesheet")
               script(src = "/js/bootstrap.min.js") {}
               script(src = "/js/util.js") {}
           }
           body {
               header("Authors")
               div {
                   id = "content"
                   h2 { +"Our Books' Authors" }
                   ul {
                       authors.forEach { author ->
                           li {
                               form(method = FormMethod.post, action = "/authors/${author.id}/delete") {
                                   style = "margin-block-end: 1em;"
                                   onSubmit = "return confirmDelete('author', \"${author.name}\")"
                                   a(href = "/authors/${author.id}") {
                                       +author.name
                                       style = "margin-right: 0.25em;"
                                   }
                                   button(type = ButtonType.submit, classes = "btn btn-danger") { +"Delete" }
                               }
                           }
                       }
                   }
                   a(classes = "btn btn-primary", href = "/authors/add") { +"Add Author" }
               }
               footer()
           }
       }
   }
}

Enter fullscreen mode Exit fullscreen mode

Note that – aside from the obvious difference in how the HTML code is generated – the variables that the HTML code requires (the authors variable, in this case) are injected directly into the generated code without needing a model object to transfer the data from the back-end code to an HTML file outside of the code structure.

  • In a similar vein, the fragment code is placed within a common code file; this is also the source of the writePage()helper function for reducing the boilerplate code in each page rendering function.

inline fun writePage(crossinline block : HTML.() -> Unit): String {
   return createHTMLDocument().html {
       lang = "en"
       visit(block)
   }.serialize()
}

fun FlowContent.header(pageName: String) {
   nav(classes = "navbar navbar-expand-lg navbar-dark bg-dark") {
       div(classes = "container-fluid") {
           a(href = "/", classes="navbar-brand") { +"Test Bookstore" }
           button(classes = "navbar-toggler", type = ButtonType.button) {
               attributes["data-bs-toggle"] = "collapse"
               attributes["data-bs-target"] = "#navbarHeader"
               span(classes="navbar-toggler-icon")
           }
           div(classes="collapse navbar-collapse") {
               id = "navbarHeader"

               ul(classes = "navbar-nav me-auto mb-2 mb-lg-0") {
                   li(classes = "nav-item") {
                       a(classes = "nav-link${if (pageName == "Home") " active" else ""}", href = "/") { +"Home" }
                   }
                   li(classes = "nav-item") {
                       a(classes = "nav-link${if (pageName == "Authors") " active" else ""}", href = "/authors") { +"Authors" }
                   }
                   li(classes = "nav-item") {
                       a(classes = "nav-link${if (pageName == "Books") " active" else ""}", href = "/books") { +"Books" }
                   }
               }
           }
       }
   }
}

fun FlowContent.footer() {
   footer(classes = "footer mt-auto py-3 bg-light fixed-bottom") {
       p(classes = "text-center") { +"Copyright 20XX Bookstore Productions - All Rights Reserved" }
   }
}

Enter fullscreen mode Exit fullscreen mode
  • Next, the endpoint functions in the web controller(s) will now return a response body with the MIME type of text/html.It is here where the classes that directly render the HTML code will be invoked.

@Controller
@RequestMapping("/authors")
class AuthorController(
   private val viewAuthorsPageRenderer: ViewAuthorsPageRenderer,
   private val viewAuthorPageRenderer: ViewAuthorPageRenderer,
   private val addAuthorPageRenderer: AddAuthorPageRenderer,
   private val authorService: AuthorService
) {
   @GetMapping(produces = [TEXT_HTML])
   @ResponseBody
   fun getAll() = viewAuthorsPageRenderer.renderPage()

   @GetMapping(value = ["/{id}"], produces = [TEXT_HTML])
   @ResponseBody
   fun get(@PathVariable id: Int) = viewAuthorPageRenderer.renderPage(id)

   @GetMapping(value = ["/add"], produces = [TEXT_HTML])
   @ResponseBody
   fun add(): String = addAuthorPageRenderer.renderPage()

   @PostMapping(value = ["/save"], produces = [TEXT_HTML])
   @ResponseBody
   fun save(@Valid authorForm: AuthorForm, bindingResult: BindingResult, httpServletResponse: HttpServletResponse): String {
       return if (!bindingResult.hasErrors()) {
           authorService.save(authorForm)
           httpServletResponse.sendRedirect("/authors")
           ""
       } else {
           val errors = bindingResult.allErrors.toFieldErrorsMap()
           addAuthorPageRenderer.renderPage(errors)
       }
   }

   @PostMapping(value = ["/{id}/delete"])
   fun delete(@PathVariable id: Int, httpServletResponse: HttpServletResponse) {
       authorService.delete(id)
       httpServletResponse.sendRedirect("/authors")
   }
}

Enter fullscreen mode Exit fullscreen mode

Note that redirecting web requests work differently in this approach as well. Instead of returning a string with an endpoint prefixed with redirect:, it is necessary to invoke the function HttpServletResponse.sendRedirect()– this will override any response body returned by the function, meaning that the empty string returned in the controller function save()is ultimately ignored.

  • Error handling in this approach requires slightly more code. A separate controller needs to be created and marked as the general error-handling controller:

@Controller
class BookstoreErrorController(private val errorPageRenderer: ErrorPageRenderer) : ErrorController {
   @RequestMapping("/error", produces = [TEXT_HTML])
   @ResponseBody
   fun handleError(request: HttpServletRequest): String {
       val statusCode = request.getAttribute("jakarta.servlet.error.status_code") as Int
       return errorPageRenderer.renderPage(statusCode)
   }
}

Enter fullscreen mode Exit fullscreen mode

After which the same workflow as above is required: create a class that will render the HTML code and return it as the response body for the error-handling endpoint.

@Service
class ErrorPageRenderer {
   fun renderPage(status: Int): String {
       return writePage {
           head {
               title("Bookstore - Add Author")
               link(href = "/css/bootstrap.min.css", rel = "stylesheet")
               script(src = "/js/bootstrap.min.js") {}
           }
           body {
               header("Error")
               h2 { +"Oops!" }
               p { +"An error occurred and provided the status $status" }
               footer()
           }
       }
   }
}

Enter fullscreen mode Exit fullscreen mode
  • Conducting requests against a warmed-up instance of this website produces a similar average load time of ~43ms and 219 kilobytes of bandwidth used (again, mostly for the Bootstrap Javascript and CSS files):

rUMCsoN1auWPO05nGVK9M UTCpv3nwzKZzRP4KmkVUqS0DFusMCjFVZeePwx9jBuuP1oC 6LRIZdXGexT XhfrJZbECe7fqFSB6R7IFpJpQ

Remarks

When compared to Thymeleaf, kotlinx.html offers some nice benefits for server-side web page generation:

  • The Kotlin-based DSL for generating HTML code in kotlinx.html is far less verbose than the HTML template files that are required for Thymeleaf.
  • It is possible to leverage Kotlin’s type safety and parameter/function calls to be assured that one is writing “correct” HTML code with kotlinx.html at compile time, whereas typos in the HTML code with Thymeleaf are only discoverable at runtime.
  • Naturally, all of Kotlin’s base functionality – inline functions, null safety, extension functions, etc – are available to the developer as well when writing the HTML generation code.
  • Incorporating the HTML generation code directly into the project class structure – as opposed to the two-step of having the HTML template files reside in the resources directory for Thymeleaf – means a more intuitive code flow and eliminates the potential for errors like forgetting to transfer variables from the controller to the HTML template files that is present in Thymeleaf.

However, kotlinx.html does pose some drawbacks as well when compared to Thymeleaf:

  • The most obvious drawback is that kotlinx.html is a very new technology compared to Thymeleaf – it is still in the beta stage as of writing this article compared to the years that Thymeleaf has as a production-ready library – meaning not just elevated chances for bugs, but also much less community support and general awareness.
  • There are some poignant differences between how to declare tag attributes in HTML code versus the kotlinx.html DSL. For example, the id attribute for HTML tags needs to be declared within the accompanying scope function of the tag declaration instead of within the tag declaration itself (see the div id declaration of “navbarHeader” in the kotlinx.html above).
  • Writing script and style blocks within the HTML code is a bit clunky due to how kotlinx.html auto-escapes normal text and may be better avoided in favor of declaring the code in a separate file. This explains the reference to util.jsin ViewAuthorsPageRenderer (and having to download it in the subsequent web request) instead of embedding the Javascript code for confirming the deletion of an author.
  • The flip side of having the HTML generation code directly incorporated into the project class structure means that there’s no “hot reloading” available: if an error is discovered in the HTML code, the entire server will have to be reloaded as well, something that can slow down development time.

In spite of these issues, the strengths of kotlinx.html warrant it a consideration for any developer that’s looking for an alternative to client-side website development for their application, as it imparts the very strengths that have made Kotlin a first-class citizen in JVM ecosystems like Spring Boot, Gradle, and more in web page development. More tools for solving development problems are always a plus compared to fewer tools, and who knows – it might even make web page development more “fun” as well!

Next Up

As mentioned above, the fact that the HTML generation code of kotlinx.html is located within the project class structure could translate to having to wait seconds (or more!) for the Spring Boot web application to restart before one can view any changes made to the HTML code compared to the “hot reloading” that’s available in templating engines like Thymeleaf. However, what if this weren’t the case? The next article of this two-part about this Server-Side Web Pages With Kotlin series will explore another Kotlin technology that could address this issue. Keep an eye on Apiumhub s blog as it will be published soon.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player