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

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:

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

For the full example, see the 2_static project.

Ktor

In Ktor, use the staticFiles() function to map any request made to the / path to the public physical folder. This 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 { 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:

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

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

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

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

For the full example, see the 3_router project.

Ktor

In Ktor, a common pattern is to use extension functions on the Routing 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.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 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') } })

For the full example, see the 4_parameters project.

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

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

For the full example, see the 4_parameters project.

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

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

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

For the full example, see the 5_send_response project.

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

For the full example, see the 5_send_response project.

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

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

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 redirect function:

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 respondRedirect to send a redirection response:

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

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

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:

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

For the full example, see the 7_receive_request project.

Ktor

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

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:

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

For the full example, see the 7_receive_request project.

Ktor

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

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

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

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

For the full example, see the 7_receive_request project.

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

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:

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

For the full example, see the 7_receive_request project.

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

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:

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

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

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 onCall to implement request logging:

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.

Last modified: 22 August 2024