This template hasn't been updated in 4 years. I recommend using the official spring boot starter, and try and extract some of these concepts into your own template. You should look into using something different than create-react-app as well, maybe Vite.
This is an example of a Spring Boot 2 server built with Kotlin, that automatically bundles and serves a create-react-app web-app when build.
The modules are structured in a way that makes it easy to use IntelliJ IDEA for both modules, or if you prefer you can open them modules individually with your favourite tools.
Uses Spek for testing in Kotlin.
Want GraphQL instead of REST? Check out the GraphQL branch on this repo
When developing you want to use the dev server that create-react-app provides. All XHR requests from the web-app are proxied to the backend. A typical development workflow will involve running the Spring Boot server and the create-react-app server separately.
- Run Spring Boot server, either through
./gradlew bootRun
in theserver
-folder or through IntelliJ's default run configuration. - Run create-react-app dev server by running
yarn start
in theweb
-folder.
Open http://localhost:3000
(create-react-app dev server) in your browser.
Note that any requests made to http://localhost:3000/api/...
are proxied directly to Spring Boot that is running on http://localhost:8082/api/...
. To make any requests directly to the API use 8082
, but when doing XHR requests from the web app use relative paths with /api
.
To produce a single build artifact, with spring boot that is a single .jar
-file, simply run ./gradlew build
in the server
-folder.
This will only succeed if all your tests both in server and web are green, and both build fine.
Run the compiled artifact:
java -jar build/libs/server-0.0.1-SNAPSHOT.jar
To use this as a template do the following:
- Pull the repository
- Delete the
.git
folder - In
server/build.gradle
changegroup = 'run.karl'
togroup = 'your.group'
- Select the folder
run.karl.starter
in IntelliJ, hit Shift+F6 (rename), chooseRENAME PACKAGE
and rename it to the package of your choice. Make sure the test folder was renamed as well. - Run
./gradlew build
in theserver
-folder to verify that everything is working.
The template is now clean and ready to use.
I wrote a full, beginner-friendly blog post on this. Some of the more important points a bit more terse:
If you already have a Spring Boot project running with Kotlin from Spring Initializr, and you would like to look at what this template has done instead, I'll go through the following steps:
- Serving a SPA (single page application) from Spring Boot 2
- Building and bundling the web-app into the server artifact
- Testing Spring Boot with Spek, testing dependency injected fields.
SPAs handle their own routing, which means that the Spring Boot server needs to route any request to index.html, but keep the path and query-params intact. It should also look completely transparent to the user that any redirect has happened.
This can be tricky to do in Spring Boot without having to define every single static file you want to serve. The simple solution is to configure an error page to redirect to /
. This is configured in WebConfig.kt, by simply creating a bean that adds the error page that redirects to root on any text/html 404 request.
@Bean
fun webServerFactory(): ConfigurableServletWebServerFactory {
val factory = TomcatServletWebServerFactory()
factory.errorPages.add(ErrorPage(HttpStatus.NOT_FOUND, "/"))
return factory
}
"build:gradle": "cross-env CI=true npm run test && npm run build",
"postbuild": "copyfiles -u 1 build/**/* ../server/src/main/resources/static"
Then execute build:gradle
from the gradle build with gradle-node-plugin.
Add it to your buildscript dependencies: classpath("com.moowork.gradle:gradle-node-plugin:1.2.0")
Apply the plugin: apply plugin: 'com.moowork.node'
Create tasks for installing dependencies and running build:gradle
and chain them into the gradle build
target.
task installDependencies(type: YarnTask) {
execOverrides {
it.workingDir = '../web'
}
}
task buildWeb(type: YarnTask) {
args = ['build:gradle']
execOverrides {
it.workingDir = '../web'
}
}
buildWeb.dependsOn installDependencies
build.dependsOn buildWeb
Now ./gradlew build
builds your web app, moves it to the static folder and then builds the Spring Boot artifact. Ship it!
I found the easiest way to test the Spring Boot Kotlin code with dependency injection is to use Spek, hamkrest, mockito-kotlin and use constructor autowiring.
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.jetbrains.spek:spek-api:1.1.5')
testCompile('com.natpryce:hamkrest:1.4.2.2')
testCompile 'com.nhaarman:mockito-kotlin:1.5.0'
testRuntime('org.jetbrains.spek:spek-junit-platform-engine:1.1.5')
Controller and service example:
/* src/main/run/karl/starter/example/ExampleService.kt */
@Service
class ExampleService {
data class ExampleResponse(val message: String)
fun getSomeValue() = ExampleResponse("Hello Service!")
}
/* src/main/run/karl/starter/example/ExampleController.kt */
@RestController()
class ExampleController @Autowired constructor(
private val exampleService: ExampleService
) {
@GetMapping("/api/hello", produces = ["application/json"])
fun example() = exampleService.getSomeValue()
}
Note the constructor-autowiring in the controller. To test mock the service:
object ExampleControllerSpec : Spek({
describe("a very good controller") {
// Create a mock of ExampleService
val mockedExampleService = mock<ExampleService> {
/* Define that when getSomeValue is invoked, return this
value instead of executing the original code */
on { getSomeValue() } doReturn ExampleService.ExampleResponse("Mocked Message Wahoo!")
}
/* Specify that we want to use our mocked
ExampleService by passing it in as a named parameter */
val controller = ExampleController(exampleService = mockedExampleService)
it("should return invoke service but return mocked message") {
val value = controller.example()
assert.that(value.message, equalTo("Mocked Message Wahoo!") and endsWith("Wahoo!"))
}
}
})
I'm not a master of gradle nor Kotlin, if you see something strange feel free to open an issue or submit a pull request.