This is a blog post about making a Kotlin api using Ktor for a React frontend. We’ll server the static files through the Kotlin app also. Database used is sqlite.
Github project can be found at https://github.com/jelinden/kotlin-react-app/
First the database
fun CreateTable(tableName: String) {
var connection = DriverManager.getConnection("jdbc:sqlite:./coffees.db")
var stmt = connection.createStatement()
stmt.executeUpdate("create table if not exists "+tableName+" (" +
"id TEXT, " +
"name TEXT, " +
"price NUMERIC, " +
"weight NUMERIC, " +
"roastlevel INT" +
")")
stmt.close()
connection.close()
}
Each time we start the app, first thing we do is try to create the table if it doesn’t exist.
fun Select(query: String): Array<Coffee?> {
var conn = DriverManager.getConnection("jdbc:sqlite:./coffees.db")
var stmt = conn.createStatement()
var rs = stmt.executeQuery(query)
var coffees: Array<Coffee?> = emptyArray()
while (rs.next()) {
var id = rs.getString("id")
var name = rs.getString("name")
var price = rs.getDouble("price")
var weight = rs.getDouble("weight")
var roastlevel = rs.getInt("roastlevel")
if(id != null) {
coffees = append(coffees, Coffee(id, name, price, weight, roastlevel))
}
}
stmt.close()
conn.close()
return coffees
}
Queries and database executes are quite simple. We also map the database object to a Kotlin object straight away.
Coffee domain object
data class Coffee(
val id: String,
val name: String,
val price: Double,
val weight: Double,
val roastlevel: Int
) {
constructor(name: String, price: Double, weight: Double, roastlevel: Int) : this(RandomString(16), name, price, weight, roastlevel)
override fun toString(): String = id + ' ' + name + ' ' + price + ' ' + weight + ' ' + roastlevel
}
Nothing special, couple constructors, one with an id and another one without.
A bit about testing
Before running a test, we create a test table and insert one row.
@BeforeTest
fun before() {
db.CreateTable(testTableName)
db.Insert("insert into " + testTableName + " values(?,?,?,?,?);",
listOf<Any>("abc", "name", 5.8, 0.700, 3))
}
Here’s an example test where we delete a row with an id and then do a find with that id and assert that it didn’t return anything.
@Test
fun testRemoveCoffee_03() {
val resp = db.Insert("delete from "+testTableName+" where id = ?;", listOf<Any>("abc"))
println("testRemoveCoffee " + resp)
val arr = db.Select("select * from "+testTableName+";") as Array<Coffee>
assertTrue(arr.size == 0, "after delete table should be empty")
}
Routes
Static files
static("/") {
staticRootFolder = File(path)
files("build")
default("build/index.html")
}
static("/edit") {
staticRootFolder = File(path)
files("build")
default("build/index.html")
}
We have two cases where we want to share the static files. Root and /edit. Because it’s a SPA, we need to give static files for each path we are sharing from the React app.
Api endpoints
get("/coffees") {
val coffees = db.Select("select * from coffee;") as Array<Coffee>
call.respondText(toJson(coffees), ContentType.Application.Json, HttpStatusCode.OK)
}
Getting all the coffees is a simple query. At this point we don’t mind about paging or the fact that the list might be really long.
post("/coffee") {
val coffee = call.receive<Coffee>()
db.Insert("insert into coffee values (?,?,?,?,?)"
, listOf<Any>(
RandomString(16),
coffee.name,
coffee.price,
coffee.weight,
coffee.roastlevel
)
)
call.respondText("OK", status = HttpStatusCode.Created)
}
Endpoint for inserting one coffee is a straight forward one also.
Building and running
To build ./gradlew build
and ./gradlew run
to run it. Or just use java to run: env=prod java -jar kotlin-react-0.0.1.jar
. When you build it the jar file will be at build/libs directory.
Frontend React app
Routing
We have a simple routing for the edit page.
function AppRouter() {
return (
<Router>
<Routes>
<Route path="/" element={<App />} />
<Route path="/edit" element={<UpdateCoffee />} />
</Routes>
</Router>
);
}
Listing coffees
Getting coffee list in CoffeeList.tsx
const getData=()=>{
fetch('/coffees',
{
headers : {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
}
).then(response => response.json())
.then(data => {
const coffeeArray:Coffee[] = data.data
setData(coffeeArray);
});
};
And then printing out the list:
{
data && data.length > 0 && data.map((item, i) =>
<tr key={i}>
<td><Link
to={{pathname: "/edit", search: `?id=${item.id}`}}
state={{coffee: item}}>{item.id}</Link></td>
<td>{item.name}</td>
<td>{item.price}</td>
<td>{item.weight}</td>
<td>{item.roastlevel}</td>
<td><input type="submit" value="Delete" onClick={() => removeCoffee(item.id)}></input></td>
</tr>
)
}
Adding a new coffee
We have a basic form which we send to sendCoffee function on submit. There’s also a cancel button which returns to list page.
return (
<div className="App">
<header className="App-header">Edit coffee</header>
<form onSubmit={sendCoffee.bind(this)}>
<label htmlFor="id">Id</label><br/><input id="id" type="text" name="id" value={coffee.id} readOnly></input><br/>
<label htmlFor="name">Name</label><br/><input id="name" type="text" name="name" defaultValue={coffee.name}></input><br/>
<label htmlFor="price">Price</label><br/><input id="price" type="text" name="price" defaultValue={coffee.price}></input><br/>
<label htmlFor="weight">Weight</label><br/><input id="weight" type="text" name="weight" defaultValue={coffee.weight}></input><br/>
<label htmlFor="roastlevel">Roast level</label><br/>
<select id="roastlevel" name="roastlevel" defaultValue={coffee.roastlevel}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select><br/>
<input type="submit" value="Save" />
<input type="submit" onClick={(e)=>{e.preventDefault();navigate("/");}} value="Back"/>
</form>
</div>
);
SendCoffee method prevents default action as a first thing. We define the target object and then make a json which we send to backend. Response is printed to console out and we don’t tell anything in the UI if call succeeded or failed.
const sendCoffee = (e:FormEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
id: {value: string }
name: { value: string };
price: { value: string };
weight: { value: string };
roastlevel: { value: string };
};
fetch('/coffee', {
method: 'put',
body : JSON.stringify({
id: target.id.value,
name: target.name.value,
price: target.price.value,
weight: target.weight.value,
roastlevel: target.roastlevel.value
}),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
})
.then(response => {
console.log(response.status, response);
})
.catch(function(error) {
console.log(error);
});
}
Basic functionality is there to see but of course there is also room for improvement. And you can see more at https://github.com/jelinden/kotlin-react-app/
Thank you!