Ktor 3.0.0-beta-1 Help

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 express-generator tool:

npx express-generator

Ktor

Ktor provides the following ways to generate an application skeleton:

  • A Yeoman generator

    This generator allows you to specify project settings interactively and choose a set of required plugins. Alternatively, you can pass the necessary options to the yo ktor command (see yo ktor --help).

  • Web-based generator

  • Native Command Line Tool (beta)

    To create a new Ktor project, you need to pass a project name to the ktor generate command:

    ktor generate ktor-sample
  • IntelliJ IDEA Ultimate

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}/`) })

View sample on GitHub

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) }

View sample on GitHub

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:

Date: Fri, 05 Aug 2022 06:30:48 GMT X-Powered-By: Express ETag: W/"c-Lve95gjOVATpfV8EL5X4nxwjKHE"

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.

public ├── index.html ├── ktor_logo.png ├── css │ └──styles.css └── js └── script.js

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}/`) })

View sample on GitHub

Ktor

In Ktor, use the static function to map any request made to the / path to the public physical folder. The files(".") function enables serving all files from the public folder recursively.

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") } } }

View sample on GitHub

When serving static content, Express adds several response headers that might look like this:

Accept-Ranges: bytes Cache-Control: public, max-age=0 ETag: W/"181-1823feafeb1" Last-Modified: Wed, 27 Jul 2022 13:49:01 GMT

To manage these headers in Ktor, you need to install the following plugins:

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') })

View sample on GitHub

Ktor

routing { get("/") { call.respondText("GET request to the homepage") } post("/") { call.respondText("POST request to the homepage") } }

View sample on GitHub

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().

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') })

View sample on GitHub

Ktor

Ktor provides a route function, whereby you define the path and then place the verbs for that path as nested functions.

routing { route("book") { get { call.respondText("Get a random book") } post { call.respondText("Add a book") } put { call.respondText("Update the book") } } }

View sample on GitHub

Both frameworks allow you to group related routes in a single file.

Express

Express provides the express.Router class to create mountable route handlers. Suppose we have the birds.js router file in the application's directory. This router module can be loaded in the application as shown in app.js:

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}/`) })

View sample on GitHub

Ktor

In Ktor, a common pattern is to use extension functions on the Route type to define the actual routes. The sample below (Birds.kt) defines the birdsRoutes extension function. You can include corresponding routes in the application (Application.kt) by calling this function inside the routing block:

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() } }

View sample on GitHub

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 Request.params. For example, req.parameters["login"] in the code snippet below will return admin for the /user/admin path:

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') } })

View sample on GitHub

Ktor

In Ktor, route parameters are defined using the {param} syntax. You can use call.parameters to access route parameters in route handlers:

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") } } }

View sample on GitHub

The table below compares how to access the parameters of a query string.

Express

To access route parameters in Express, you can use Request.params. For example, req.parameters["login"] in the code snippet below will return admin for the /user/admin path:

app.get('/products', (req, res) => { if (req.query['price'] === 'asc') { res.send('Products from the lowest price to the highest') } })

View sample on GitHub

Ktor

In Ktor, route parameters are defined using the {param} syntax. You can use call.parameters to access route parameters in route handlers:

routing { get("/products") { if (call.request.queryParameters["price"] == "asc") { call.respondText("Products from the lowest price to the highest") } } }

View sample on GitHub

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 res.json function:

const car = {type:"Fiat", model:"500", color:"white"}; app.get('/json', (req, res) => { res.json(car) })

View sample on GitHub

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 annotation:

@Serializable data class Car(val type: String, val model: String, val color: String)

Then, you can use call.respond to send an object of this class in a response:

get("/json") { call.respond(Car("Fiat", "500", "white")) }

View sample on GitHub

File

Express

To respond with a file in Express, use res.sendFile:

const path = require("path") app.get('/file', (req, res) => { res.sendFile(path.join(__dirname, 'ktor_logo.png')) })

View sample on GitHub

Ktor

Ktor provides the call.respondFile function for sending files to the client:

get("/file") { val file = File("public/ktor_logo.png") call.respondFile(file) }

View sample on GitHub

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 res.download function transfers the specified file as an attachment:

app.get('/file-attachment', (req, res) => { res.download("ktor_logo.png") })

View sample on GitHub

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) }

View sample on GitHub

Redirect

Express

To generate a redirection response in Express, call the redirect function:

app.get('/old', (req, res) => { res.redirect(301, "moved") }) app.get('/moved', (req, res) => { res.send('Moved resource') })

View sample on GitHub

Ktor

In Ktor, use respondRedirect to send a redirection response:

get("/old") { call.respondRedirect("/moved", permanent = true) } get("/moved") { call.respondText("Moved resource") }

View sample on GitHub

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 res.render:

app.set('views', './views') app.set('view engine', 'pug') app.get('/', (req, res) => { res.render('index', { title: 'Hey', message: 'Hello there!' }) })

View sample on GitHub

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 FreeMarker plugin and then send a template using call.respond:

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)

View sample on GitHub

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:

POST http://0.0.0.0:3000/text Content-Type: text/plain Hello, world!

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 body-parser:

const bodyParser = require('body-parser')

In the post handler, you need to pass the text parser (bodyParser.text). A request body will be available under the req.body property:

app.post('/text', bodyParser.text(), (req, res) => { let text = req.body res.send(text) })

View sample on GitHub

Ktor

In Ktor, you can receive a body as a text using call.receiveText:

post("/text") { val text = call.receiveText() call.respondText(text) }

View sample on GitHub

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:

POST http://0.0.0.0:3000/json Content-Type: application/json { "type": "Fiat", "model" : "500", "color": "white" }

Express

To receive JSON in Express, use bodyParser.json:

const bodyParser = require('body-parser') app.post('/json', bodyParser.json(), (req, res) => { let car = req.body res.send(car) })

View sample on GitHub

Ktor

In Ktor, you need to install the ContentNegotiation plugin and configure the Json serializer:

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 receive method that accepts this data class as a parameter:

post("/json") { val car = call.receive<Car>() call.respond(car) }

View sample on GitHub

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:

POST http://localhost:3000/urlencoded Content-Type: application/x-www-form-urlencoded username=JetBrains&email=example@jetbrains.com&password=foobar&confirmation=foobar

Express

As for plain text and JSON, Express requires body-parser. You need to set the parser type to bodyParser.urlencoded:

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`) })

View sample on GitHub

Ktor

In Ktor, use the call.receiveParameters function:

post("/urlencoded") { val formParameters = call.receiveParameters() val username = formParameters["username"].toString() call.respondText("The '$username' account is created") }

View sample on GitHub

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:

POST http://localhost:3000/raw Content-Type: application/octet-stream < ./ktor_logo.png

Express

To handle binary data in Express, set the parser type to raw:

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') })

View sample on GitHub

Ktor

Ktor provides ByteReadChannel and ByteWriteChannel for asynchronous reading/writing sequences of bytes:

post("/raw") { val file = File("uploads/ktor_logo.png") call.receiveChannel().copyAndClose(file.writeChannel()) call.respondText("A file is uploaded") }

View sample on GitHub

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:

POST http://localhost:3000/multipart Content-Type: multipart/form-data; boundary=WebAppBoundary --WebAppBoundary Content-Disposition: form-data; name="description" Content-Type: text/plain Ktor logo --WebAppBoundary Content-Disposition: form-data; name="image"; filename="ktor_logo.png" Content-Type: image/png < ./ktor_logo.png --WebAppBoundary--

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}`) })

View sample on GitHub

Ktor

In Ktor, if you need to receive a file sent as a part of a multipart request, call the receiveMultipart function and then loop over each part as required. In the example below, PartData.FileItem is used to receive a file as a byte stream:

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'") }

View sample on GitHub

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 app.use:

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)

View sample on GitHub

Ktor

Ktor allows you to extend its functionality using custom plugins. The code example below shows how to handle onCall to implement request logging:

val RequestLoggingPlugin = createApplicationPlugin(name = "RequestLoggingPlugin") { onCall { call -> call.request.origin.apply { println("Request URL: $scheme://$localHost:$localPort$uri") } } }

View sample on GitHub

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.

Last modified: 02 April 2024