Guitars

Spring MVC with Pebble Templates

Feb 15, 2021

Spring, for the time being, is the biggest, fully-featured MVC framework for Kotlin. While it’s predominantly used for creating JSON API servers, you can leverage the fantastic and intuitive Pebble templating engine to rapidly build a full-stack app in Spring - here’s how!

Kotlin has a great introductory video for building a JSON API in Spring. After this you’ll probably have something that looks like this codebase with a main application file like so:

src/main/kotlin/demo/DemoApplication.kt

package pebble_spring_template

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.data.annotation.Id
import org.springframework.data.jdbc.repository.query.Query
import org.springframework.data.relational.core.mapping.Table
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class DemoApplication

fun main(args: Array<String>) {
	runApplication<DemoApplication>(*args)
}

@RestController
class MessageResource(val service: MessageService) {
	@GetMapping
	fun home(): List<Message> {
		return service.findMessages()
	}

	@PostMapping
	fun post(@RequestBody message: Message) {
		service.post(message)
	}
}

@Service
class MessageService(val db: MessageRepository) {
	fun findMessages(): List<Message> = db.findMessages()

	fun post(message: Message){
		db.save(message)
	}
}

interface MessageRepository : CrudRepository<Message, String>{
	@Query("select * from messages")
	fun findMessages(): List<Message>
}

@Table("MESSAGES")
data class Message(@Id val id: String?, val text: String)

This currently handles saving messages and displaying messages as JSON. That’s fine if we want a separated front end application to handle these, but we can also get our Spring app to handle this instead all in the same codebase.

Part One - Showing All Messages

As mentioned, Pebble is a fantastic templating engine akin to Python’s Jinja Templating engine or Ruby’s ERB templates. To use it, we first need to add it to our build.gradle.kts file:

dependencies {
  implementation("io.pebbletemplates:pebble-spring-boot-starter:3.1.4")  // Added!
	implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
	runtimeOnly("com.h2database:h2")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Don’t forget to rebuild your project after you’ve done this so you download the pebble engine into your project!

Next up, we need a pebble template to render. Let’s create a new /messages route which will take the messages from the database and, rather than serving up a json list, we’ll serve an HTML template with our messages displayed as a list:

src/main/kotlin/demo/DemoApplication.kt

@RestController
class MessageResource(val service: MessageService) {
	@GetMapping
	fun home(): List<Message> {
		return service.findMessages()
	}

        // Added!
        @GetMapping("/messages")
	fun index(): String {
		val messages = service.findMessages()
		return 'Pebble HTML template of messages goes here!'
	}

	@PostMapping
	fun post(@RequestBody message: Message) {
		service.post(message)
	}
}

For now, we’re just sending a string back. To send back a Pebble HTML template, we firstly need one. As the Pebble docs explain, the Pebble loader is expecting templates to sit in our /src/main/kotlin/resources/templates directory so let’s go ahead and add our messages index template there:

touch src/main/kotlin/resources/templates/messages_index.peb

src/main/kotlin/resources/templates/messages_list.peb

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <section>
    <h1>My List of Messages:</h1>
    <ul>
      
      {% for message in messages  %}
        <li>{{ message.text }}</li>
      {% endfor %}
      
    </ul>
  </section>
</body>
</html>

We can tell our template to expect something called messages to be passed in when this is used, this will be a list that we can iterate over using Pebble’s for loop and display the text property for each message.

Now we need to tell our newly minted Pebble templating engine to take this template, pass in the messages from our database to render real values. Let’s update our controller:

src/main/kotlin/demo/DemoApplication.kt

@GetMapping("/messages")
fun index(): String {
  val messages = service.findMessages()
  val template = PebbleEngine.Builder().build().getTemplate("templates/messages_index.peb") // Added!
  val writer = StringWriter() // Added!
  template.evaluate(writer, mapOf("messages" to messages)) // Added!
  return writer.toString() // Added!
}

This creates a new instance of the Pebble Builder allowing us to take our template and inject in our messages. These need to be in a map so we can tell the pebble template that our messages are the same messages it’s looking for.

With that, you should be able to see your messages! You’ll need to post some new ones with an API client like Insomnia before seeing any pop up.

list of messages


Part Two - Posting Messages with Forms

This is taking shape. Now we’d ideally like a way to create new messages in our app rather than using an API client of some kind so we need a route to render an HTML form to allow us to enter and save messages. We can copy and paste the same code like we did before for the first pass in our controller. Note, we’re not passing any data to our form view, so we omit the second argument for template.evaluate()

src/main/kotlin/demo/DemoApplication.kt

// Added!
@GetMapping("/messages/new")
fun new(): String {
  val template = PebbleEngine.Builder().build().getTemplate("templates/messages_new.peb")
  val writer = StringWriter()
  template.evaluate(writer)
  return writer.toString()
}

However we probably want to refactor the templating steps into its own renderTemplate function, where we can specify the template we want and an optional argument for any data we need to pass to it. This makes our controller code much cleaner too:

src/main/kotlin/demo/DemoApplication.kt

@GetMapping("/messages")
fun index(): String {
  val messages = service.findMessages()
  return renderTemplate("messages_list", messages) // Altered!
}

@GetMapping("/messages/new")
fun new(): String {
  return renderTemplate("messages_new") // Altered!
}

// Added!
private fun renderTemplate(templateName:String, templateArguments:Map<String, List<Any>>?=null):String {
  val template = PebbleEngine.Builder().build().getTemplate("templates/$templateName.peb")
  val writer = StringWriter()
  template.evaluate(writer, templateArguments)
  return writer.toString()
}

Now we can add the messages_new.peb file to our templates directory:

src/main/kotlin/resources/templates/messages_new.peb

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <form action="/messages" method="post">
      <label for="message">Message:</label>
      <input type="text" name="message" placeholder="Enter your message here">
      <input type="submit" value="Send!">
  </form>
</body>
</html>

While we do have a route for handling POST requests for messages, we’ve got to be really confident that the inputs from our form are going to be in the right format for Spring to automatically create a Message object out of it. Also, what if we want to do some extra processing/cleansing of the form inputs before we create our message object? Other frameworks wrap HTML form data into a map/hash of some kind that we can then manually extract and create the right kind of object we need, allowing for us to do any preprocessing we need along the way. We’ll mimic this with our new route:

src/main/kotlin/demo/DemoApplication.kt

// Added!
@PostMapping("/messages")
fun create(@RequestBody formData: MultiValueMap<String, String>): RedirectView {
  val message = formData["message"]!!.first()
  val newMessage = Message(text=message)
  service.post(newMessage)
  return RedirectView("/messages")
}

@Table("MESSAGES")
data class Message(@Id val id: String?=null, val text: String) // Altered!

Unlike Java, Kotlin really cares about where things could be null, like with the fields coming in from our form. Here we’re using Kotlin’s !! so that we throw an error on the incoming form data rather than accidentally sending null values to the database. Assuming a successful database save we then redirect to our messages. Once you’re comfortable with this all functioning you could definitely add in some more graceful error handling here.

Now we have a way to create a view messages in our spring app using Pebble templates! The final code for this article can be found here. From here you could create a fully functional CRUD app inside Spring without any need to worry about a separate front end app.