Migrating from Express to Ktor
In this guide, we'll take a look at how to migrate an Express application to Ktor in basic scenarios: from generating an application and writing your first application to creating middleware for extending application functionality.
Generate an app
Express | You can generate a new Express application using the
npx express-generator
|
---|---|
Ktor | Ktor provides the following ways to generate an application skeleton:
|
Hello world
In this section, we'll look at how to create the simplest server application that accepts GET
requests and responds with predefined plain text.
Express | The example below shows the Express application that starts a server and listens on port 3000 for connections. const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Responding at http://0.0.0.0:${port}/`)
})
|
---|---|
Ktor | In Ktor, you can use the embeddedServer function to configure server parameters in code and quickly run an application. import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0") {
routing {
get("/") {
call.respondText("Hello World!")
}
}
}.start(wait = true)
} You can also specify server settings in an external configuration file that uses the HOCON or YAML format. |
Note that the Express application above adds the Date, X-Powered-By, and ETag response headers that might look as follows:
To add the default Server and Date headers into each response in Ktor, you need to install the DefaultHeaders plugin. The ConditionalHeaders plugin can be used to configure the Etag response header.
Serving static content
In this section, we'll see how to serve static files such as images, CSS files, and JavaScript files in Express and Ktor. Suppose we have the public folder with the main index.html page and a set of linked assets.
Express | In Express, pass the folder name to the express.static function. const express = require('express')
const app = express()
const port = 3000
app.use(express.static('public'))
app.listen(port, () => {
console.log(`Responding at http://0.0.0.0:${port}/`)
})
|
---|---|
Ktor | In Ktor, use the static function to map any request made to the / path to the public physical folder. The import io.ktor.server.application.*
import io.ktor.server.http.content.*
import io.ktor.server.routing.*
import java.io.*
fun main(args: Array<String>): Unit =
io.ktor.server.netty.EngineMain.main(args)
fun Application.module() {
routing {
static("/") {
staticRootFolder = File("public")
files(".")
default("index.html")
}
}
} |
When serving static content, Express adds several response headers that might look like this:
To manage these headers in Ktor, you need to install the following plugins:
Accept-Ranges: PartialContent
Cache-Control: CachingHeaders
ETag and Last-Modified: ConditionalHeaders
Routing
Routing allows handling incoming requests made to a particular endpoint, which is defined by a specific HTTP request method (GET
, POST
, and so on) and a path. The examples below show how to handle GET
and POST
requests made to the / path.
Express | app.get('/', (req, res) => {
res.send('GET request to the homepage')
})
app.post('/', (req, res) => {
res.send('POST request to the homepage')
}) |
---|---|
Ktor | routing {
get("/") {
call.respondText("GET request to the homepage")
}
post("/") {
call.respondText("POST request to the homepage")
}
} |
The following examples demonstrate how to group route handlers by paths.
Express | In Express, you can create chainable route handlers for a route path by using app.route('/book')
.get((req, res) => {
res.send('Get a random book')
})
.post((req, res) => {
res.send('Add a book')
})
.put((req, res) => {
res.send('Update the book')
}) |
---|---|
Ktor | Ktor provides a routing {
route("book") {
get {
call.respondText("Get a random book")
}
post {
call.respondText("Add a book")
}
put {
call.respondText("Update the book")
}
}
} |
Both frameworks allow you to group related routes in a single file.
Express | Express provides the const express = require('express')
const router = express.Router()
router.get('/', (req, res) => {
res.send('Birds home page')
})
router.get('/about', (req, res) => {
res.send('About birds')
})
module.exports = router
const express = require('express')
const app = express()
const birds = require('./birds')
const port = 3000
app.use('/birds', birds)
app.listen(port, () => {
console.log(`Responding at http://0.0.0.0:${port}/`)
}) |
---|---|
Ktor | In Ktor, a common pattern is to use extension functions on the import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Route.birdsRoutes() {
route("/birds") {
get {
call.respondText("Birds home page")
}
get("/about") {
call.respondText("About birds")
}
}
} import com.example.routes.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun main(args: Array<String>): Unit =
io.ktor.server.netty.EngineMain.main(args)
fun Application.module() {
routing {
birdsRoutes()
}
} |
Apart from specifying URL paths as strings, Ktor includes the capability to implement type-safe routes.
Route and query parameters
This section will show us how to access route and query parameters.
A route (or path) parameter is a named URL segment used to capture the value specified at its position in the URL.
Express | To access route parameters in Express, you can use app.get('/user/:login', (req, res) => {
if (req.params['login'] === 'admin') {
res.send('You are logged in as Admin')
} else {
res.send('You are logged in as Guest')
}
}) |
---|---|
Ktor | In Ktor, route parameters are defined using the routing {
get("/user/{login}") {
if (call.parameters["login"] == "admin") {
call.respondText("You are logged in as Admin")
} else {
call.respondText("You are logged in as Guest")
}
}
} |
The table below compares how to access the parameters of a query string.
Express | To access route parameters in Express, you can use app.get('/products', (req, res) => {
if (req.query['price'] === 'asc') {
res.send('Products from the lowest price to the highest')
}
}) |
---|---|
Ktor | In Ktor, route parameters are defined using the routing {
get("/products") {
if (call.request.queryParameters["price"] == "asc") {
call.respondText("Products from the lowest price to the highest")
}
}
} |
Sending responses
In the previous sections, we've already seen how to respond with plain text content. Let's see how to send JSON, file, and redirect responses.
JSON
Express | To send a JSON response with the appropriate content type in Express, call the const car = {type:"Fiat", model:"500", color:"white"};
app.get('/json', (req, res) => {
res.json(car)
}) |
---|---|
Ktor | In Ktor, you need to install the ContentNegotiation plugin and configure the JSON serializer: install(ContentNegotiation) {
json()
} To serialize data into JSON, you need to create a data class with the @Serializable
data class Car(val type: String, val model: String, val color: String) Then, you can use get("/json") {
call.respond(Car("Fiat", "500", "white"))
} |
File
Express | To respond with a file in Express, use const path = require("path")
app.get('/file', (req, res) => {
res.sendFile(path.join(__dirname, 'ktor_logo.png'))
}) |
---|---|
Ktor | Ktor provides the get("/file") {
val file = File("public/ktor_logo.png")
call.respondFile(file)
} |
The Express application adds the Accept-Ranges HTTP response header when responding with a file. The server uses this header for advertising its support for partial requests from the client for file downloads. In Ktor, you need to install the PartialContent plugin to support partial requests.
File attachment
Express | The app.get('/file-attachment', (req, res) => {
res.download("ktor_logo.png")
}) |
---|---|
Ktor | In Ktor, you need to configure the Content-Disposition header manually to transfer the file as the attachment: get("/file-attachment") {
val file = File("public/ktor_logo.png")
call.response.header(
HttpHeaders.ContentDisposition,
ContentDisposition.Attachment.withParameter(ContentDisposition.Parameters.FileName, "ktor_logo.png")
.toString()
)
call.respondFile(file)
} |
Redirect
Express | To generate a redirection response in Express, call the app.get('/old', (req, res) => {
res.redirect(301, "moved")
})
app.get('/moved', (req, res) => {
res.send('Moved resource')
}) |
---|---|
Ktor | In Ktor, use get("/old") {
call.respondRedirect("/moved", permanent = true)
}
get("/moved") {
call.respondText("Moved resource")
} |
Templates
Both Express and Ktor enable working with template engines for working with views.
Express | Suppose we have the following Pug template in the views folder: html
head
title= title
body
h1= message To respond with this template, call app.set('views', './views')
app.set('view engine', 'pug')
app.get('/', (req, res) => {
res.render('index', { title: 'Hey', message: 'Hello there!' })
}) |
---|---|
Ktor | Ktor supports several JVM template engines, such as FreeMarker, Velocity, and so on. For example, if you need to respond with a FreeMarker template placed in application resources, install and configure the fun Application.module() {
install(FreeMarker) {
templateLoader = ClassTemplateLoader(this::class.java.classLoader, "views")
}
routing {
get("/") {
val article = Article("Hey", "Hello there!")
call.respond(FreeMarkerContent("index.ftl", mapOf("article" to article)))
}
}
}
data class Article(val title: String, val message: String) |
Receiving requests
This section will show how to receive request bodies in different formats.
Raw text
The POST
request below sends text data to the server:
Let's see how to receive a body of this request as plain text on the server side.
Express | To parse an incoming request body in Express, you need to add const bodyParser = require('body-parser') In the app.post('/text', bodyParser.text(), (req, res) => {
let text = req.body
res.send(text)
}) |
---|---|
Ktor | In Ktor, you can receive a body as a text using post("/text") {
val text = call.receiveText()
call.respondText(text)
} |
JSON
In this section, we'll look at how to receive a JSON body. The sample below shows a POST
request with a JSON object in its body:
Express | To receive JSON in Express, use const bodyParser = require('body-parser')
app.post('/json', bodyParser.json(), (req, res) => {
let car = req.body
res.send(car)
}) |
---|---|
Ktor | In Ktor, you need to install the ContentNegotiation plugin and configure the install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
})
} To deserialize received data into an object, you need to create a data class: @Serializable
data class Car(val type: String, val model: String, val color: String) Then, use the post("/json") {
val car = call.receive<Car>()
call.respond(car)
} |
URL-encoded
Now let's see how to receive form data sent using the application/x-www-form-urlencoded type. The code snippet below shows a sample POST
request with form data:
Express | As for plain text and JSON, Express requires const bodyParser = require('body-parser')
app.post('/urlencoded', bodyParser.urlencoded({extended: true}), (req, res) => {
let user = req.body
res.send(`The ${user["username"]} account is created`)
}) |
---|---|
Ktor | In Ktor, use the post("/urlencoded") {
val formParameters = call.receiveParameters()
val username = formParameters["username"].toString()
call.respondText("The '$username' account is created")
} |
Raw data
The next use case is handling binary data. The request below sends a PNG image with the application/octet-stream to the server:
Express | To handle binary data in Express, set the parser type to const bodyParser = require('body-parser')
const fs = require('fs')
app.post('/raw', bodyParser.raw({type: () => true}), (req, res) => {
let rawBody = req.body
fs.createWriteStream('./uploads/ktor_logo.png').write(rawBody)
res.send('A file is uploaded')
}) |
---|---|
Ktor | Ktor provides post("/raw") {
val file = File("uploads/ktor_logo.png")
call.receiveChannel().copyAndClose(file.writeChannel())
call.respondText("A file is uploaded")
} |
Multipart
In the final section, let's look at how to handle multipart bodies. The POST
request below sends a PNG image with a description using the multipart/form-data type:
Express | Express requires a separate module to parse multipart data. In the example below, multer is used to upload a file to the server: const multer = require('multer')
const storage = multer.diskStorage({
destination: './uploads/',
filename: function (req, file, cb) {
cb(null, file.originalname);
}
})
const upload = multer({storage: storage});
app.post('/multipart', upload.single('image'), function (req, res, next) {
let fileDescription = req.body["description"]
let fileName = req.file.filename
res.send(`${fileDescription} is uploaded to uploads/${fileName}`)
}) |
---|---|
Ktor | In Ktor, if you need to receive a file sent as a part of a multipart request, call the post("/multipart") {
var fileDescription = ""
var fileName = ""
val multipartData = call.receiveMultipart()
multipartData.forEachPart { part ->
when (part) {
is PartData.FormItem -> {
fileDescription = part.value
}
is PartData.FileItem -> {
fileName = part.originalFileName as String
var fileBytes = part.streamProvider().readBytes()
File("uploads/$fileName").writeBytes(fileBytes)
}
else -> {}
}
}
call.respondText("$fileDescription is uploaded to 'uploads/$fileName'")
} |
Creating middleware
The final thing we'll look at is how to create middleware that allows you to extend the server functionality. The examples below show how to implement request logging using Express and Ktor.
Express | In Express, middleware is a function bound to the application using const express = require('express')
const app = express()
const port = 3000
const requestLogging = function (req, res, next) {
let scheme = req.protocol
let host = req.headers.host
let url = req.url
console.log(`Request URL: ${scheme}://${host}${url}`)
next()
}
app.use(requestLogging) |
---|---|
Ktor | Ktor allows you to extend its functionality using custom plugins. The code example below shows how to handle val RequestLoggingPlugin = createApplicationPlugin(name = "RequestLoggingPlugin") {
onCall { call ->
call.request.origin.apply {
println("Request URL: $scheme://$localHost:$localPort$uri")
}
}
} |
What's next
There are still a lot of use cases not covered in this guide, such as session management, authorization, database integration, and so on. For most of these functionalities, Ktor provides dedicated plugins that can be installed in the application and configured as required. To continue your journey with Ktor, visit the Learn page, which provides a set of step-by-step guides and ready-to-use samples.