Back to the Future: Server-Side Web Pages With Kotlin (Pt. 1)
Server-side, web page rendering, is making a comeback; here's a look at how an emerging Kotlin library could be used for this purpose.
Join the DZone community and get the full member experience.
Join For FreeWeb 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 “backend” development that contained the traditional web server and business logic code along with “front-end” development (using the frameworks above) 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. Again, 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 continued 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 fewer 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 “backend” 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 various 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, 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"
}
}
}
}
This results in the following webpage:
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 theNode
class in the example code (which is a basic translation of theNode
class 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. Still, the benefit is that it allows for cleaner-looking function calls. - Scope Functions: Scope functions 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 the
href
attribute while leaving the remaining arguments of the tag declaration –target
andclasses
– 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 totext()
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>
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>
- 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"
}
}
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>
- 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):
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-html
to 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()
}
}
}
}
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) is injected directly into the generated code without needing a model
object to transfer the data from the backend 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" }
}
}
- 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")
}
}
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)
}
}
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()
}
}
}
}
- 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):
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 are 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.js
inViewAuthorsPageRenderer
(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 this, “Server-Side Web Pages With Kotlin,” series will explore another Kotlin technology that could address this issue.
Published at DZone with permission of Severn Everett. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments