Thoughts of a software developer

08.05.2022 21:55 | Modified 03.04. 13:31
Kotlin api with React frontend

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!