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}/`)
})
For the full example, see the 1_hello project. |
---|---|
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)
} For the full example, see the 1_hello project. 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}/`)
})
For the full example, see the 2_static project. |
---|---|
Ktor | In Ktor, use 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 {
staticFiles("", File("public"), "index.html")
}
} For the full example, see the 2_static project. |
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')
}) For the full example, see the 3_router project. |
---|---|
Ktor | routing {
get("/") {
call.respondText("GET request to the homepage")
}
post("/") {
call.respondText("POST request to the homepage")
}
} For the full example, see the 3_router project. |
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')
}) For the full example, see the 3_router project. |
---|---|
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")
}
}
} For the full example, see the 3_router project. |
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}/`)
}) For the full example, see the 3_router project. |
---|---|
Ktor | In Ktor, a common pattern is to use extension functions on the import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Routing.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()
}
} For the full example, see the 3_router project. |
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')
}
}) For the full example, see the 4_parameters project. |
---|---|
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")
}
}
} For the full example, see the 4_parameters project. |
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')
}
}) For the full example, see the 4_parameters project. |
---|---|
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")
}
}
} For the full example, see the 4_parameters project. |
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)
}) For the full example, see the 5_send_response project. |
---|---|
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"))
} For the full example, see the 5_send_response project. |
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'))
}) For the full example, see the 5_send_response project. |
---|---|
Ktor | Ktor provides the get("/file") {
val file = File("public/ktor_logo.png")
call.respondFile(file)
} For the full example, see the 5_send_response project. |
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")
}) For the full example, see the 5_send_response project. |
---|---|
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)
} For the full example, see the 5_send_response project. |
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')
}) For the full example, see the 5_send_response project. |
---|---|
Ktor | In Ktor, use get("/old") {
call.respondRedirect("/moved", permanent = true)
}
get("/moved") {
call.respondText("Moved resource")
} For the full example, see the 5_send_response project. |
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!' })
}) For the full example, see the 6_templates project. |
---|---|
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) For the full example, see the 6_templates project. |
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)
}) For the full example, see the 7_receive_request project. |
---|---|
Ktor | In Ktor, you can receive a body as a text using routing {
post("/text") {
val text = call.receiveText()
call.respondText(text) For the full example, see the 7_receive_request project. |
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)
}) For the full example, see the 7_receive_request project. |
---|---|
Ktor | In Ktor, you need to install the ContentNegotiation plugin and configure the fun Application.module() {
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
}) To deserialize received data into an object, you need to create a data class:
@Serializable Then, use the }
post("/json") {
val car = call.receive<Car>()
call.respond(car) For the full example, see the 7_receive_request project. |
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`)
}) For the full example, see the 7_receive_request project. |
---|---|
Ktor | In Ktor, use the }
post("/urlencoded") {
val formParameters = call.receiveParameters()
val username = formParameters["username"].toString()
call.respondText("The '$username' account is created") For the full example, see the 7_receive_request project. |
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')
}) For the full example, see the 7_receive_request project. |
---|---|
Ktor | Ktor provides }
post("/raw") {
val file = File("uploads/ktor_logo.png")
call.receiveChannel().copyAndClose(file.writeChannel())
call.respondText("A file is uploaded") For the full example, see the 7_receive request project. |
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}`)
}) For the full example, see the 7_receive_request project. |
---|---|
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
val fileBytes = part.provider().readRemaining().readByteArray()
File("uploads/$fileName").writeBytes(fileBytes)
}
else -> {}
}
part.dispose()
} For the full example, see the 7_receive_request project. |
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) For the full example, see the 8_middleware project. |
---|---|
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")
}
}
} For the full example, see the 8_middleware project. |
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.