{"id":512597,"date":"2024-09-24T11:13:47","date_gmt":"2024-09-24T10:13:47","guid":{"rendered":"https:\/\/blog.jetbrains.com\/?post_type=idea&#038;p=512597"},"modified":"2024-10-15T19:35:18","modified_gmt":"2024-10-15T18:35:18","slug":"ktor-101-efficient-jvm-http-toolkit","status":"publish","type":"idea","link":"https:\/\/blog.jetbrains.com\/idea\/2024\/09\/ktor-101-efficient-jvm-http-toolkit\/","title":{"rendered":"Ktor 101: Efficient JVM HTTP Toolkit"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Introduction<\/h2>\n\n\n\n<p>As an experienced Java developer, you&#8217;re likely familiar with robust, full-featured web frameworks, like Spring Boot. Today, we&#8217;ll explore <a href=\"https:\/\/ktor.io\" target=\"_blank\" rel=\"noopener\">Ktor<\/a>, a toolkit for building server applications on JVM with Kotlin. Ktor was designed from the ground up to take advantage of Kotlin\u2019s features, including coroutines, to build efficient and flexible web applications.<\/p>\n\n\n\n<p>As a web application toolkit, Ktor provides the essential components for building the application, like routing, authentication, and utilities for working with various protocols, including HTTP and WebSockets. For other use cases, such as working with databases, developers are free to pick any libraries that suit their needs.<\/p>\n\n\n\n<p>In this article, we provide a quick overview of Ktor\u2019s features to help you get started with this tool.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The \u201cHello, World!\u201d app<\/h2>\n\n\n\n<p>Starting a new project with Ktor is easy. You can use the project generator available at <a href=\"https:\/\/start.ktor.io\" target=\"_blank\" rel=\"noopener\">start.ktor.io<\/a> or the <a href=\"https:\/\/ktor.io\/docs\/server-create-a-new-project.html#create_project_with_intellij\" target=\"_blank\" rel=\"noopener\"><em>New Project<\/em> wizard in IntelliJ IDEA<\/a>.<\/p>\n\n\n\n<p>The equivalent of a <strong>Hello World<\/strong> application with Ktor would be:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">fun main() {\n   embeddedServer(Netty, port = 8080, host = \"0.0.0.0\") {\n       routing {\n           get(\"\/hello\") {\n               call.respondText(\"Hello, World!\")\n           }\n       }\n   }.start(wait = true)\n}<\/pre>\n\n\n\n<p>To begin, we&#8217;re instantiating a server that utilizes <a href=\"https:\/\/netty.io\/\" target=\"_blank\" rel=\"noopener\">Netty<\/a> as its underlying engine and operates on port 8080.<\/p>\n\n\n\n<p>Next, we define a specific route to handle incoming requests. In this instance, we&#8217;re instructing the server to respond with the plain text message <code>Hello, World!<\/code> when a request is made to the <code>\/hello<\/code> path.<\/p>\n\n\n\n<p>Last, we initiate the server and instruct it to wait, thereby preventing our application from terminating immediately.<\/p>\n\n\n\n<p>That\u2019s as simple as it gets when it comes to Ktor. To implement additional functionality, we would need to define additional HTTP verbs and their corresponding URLs within the <code>routing<\/code> function. For instance, if we\u2019d like to respond to POST, we\u2019d simply add another function:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">   routing {\n      get(\"\/hello\") {\n         call.respondText(\"Hello, World!\")\n      }\n      post(\"\/hello\") {\n         \/\/ \u2026\n      }\n   }<\/pre>\n\n\n\n<p>Or we can regroup the endpoints by declaring a <code>route<\/code> and specify the HTTP verbs for that route as follows:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">   routing {\n      route(\"\/hello\") {\n          get {\n             call.respondText(\"Hello, World!\")\n          }\n          post {\n             \/\/ \u2026\n          }\n       }\n   }<\/pre>\n\n\n\n<p>Grouping endpoints into routes is useful for enabling Ktor plugins, such as content negotiation, within the scope of each route.<\/p>\n\n\n\n<p>To demonstrate how Ktor leverages Kotlin\u2019s features, let\u2019s extract the individual endpoints into <a href=\"https:\/\/kotlinlang.org\/docs\/extensions.html#extension-functions\" target=\"_blank\" rel=\"noopener\">extension functions<\/a> for the Route type:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">   routing {\n      route(\"\/hello\") {\n          getEndpoint()\n          postEndpoint()\n      }\n   }\n\n   private fun Route.getEndpoint() {\n       get { \u2026 }\n   }\n\n   private fun Route.postEndpoint() {\n       post { \u2026 }\n   }<\/pre>\n\n\n\n<p>The new functions, <code>getEndpoint<\/code> and <code>postEndpoint<\/code>, are defined as extensions of Ktor\u2019s Route class, meaning they can only be used inside the <code>route { \u2026 }<\/code> DSL block. Ktor relies heavily on <a href=\"https:\/\/kotlinlang.org\/docs\/type-safe-builders.html\" target=\"_blank\" rel=\"noopener\">type-safe builders<\/a> for its DSLs, giving the code a more declarative style.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Content negotiation<\/h2>\n\n\n\n<p>The purpose of content negotiation is to handle the conversion between different data formats when communicating between the client and the server. In Ktor, content negotiation also enables automatic serialization and deserialization of data formats such as JSON, XML, or other types, based on the <code>Content-Type<\/code> headers in HTTP requests and responses.<\/p>\n\n\n\n<p>The following example demonstrates the use of <a href=\"https:\/\/ktor.io\/docs\/serialization.html\" target=\"_blank\" rel=\"noopener\">content negotiation<\/a> with JSON serialization for the Message data class:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">@Serializable\ndata class Message(val id: String, val message: String)\n\nembeddedServer(Netty, port = 8080, host = \"0.0.0.0\") {\n   install(ContentNegotiation) {\n       json()\n   }\n   routing {\n       route(\"\/hello\") {\n           get {\n               call.respond(Message(\"123ABC\", \"Hello, World!\"))\n           }\n           post {\n               val message = call.receive&lt;Message>()\n               \/\/ do something with the message\n           }\n       }\n   }\n\n}.start(wait = true)<\/pre>\n\n\n\n<p>First, even if the dependency that implements the corresponding feature is present in the classpath of the project, in Ktor, we have to explicitly enable this functionality. This is done using the <code>install<\/code> function, with the feature name following a configuration block.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">   install(ContentNegotiation) {\n       json()\n   }<\/pre>\n\n\n\n<p>This approach avoids the implicit behavior that could be imposed by the accidental presence of a dependency artifact in the classpath.<\/p>\n\n\n\n<p>As soon as the content negotiation is configured, the endpoints are ready to interact with the intended format. In our example, the <code>Message<\/code> data class will be serialized to JSON in the GET endpoint on the <code>call.respond(...)<\/code> invocation, and deserialized in the POST endpoint on <code>call.receive&lt;Message&gt;()<\/code> invocation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Plugins<\/h2>\n\n\n\n<p>We\u2019ve already mentioned the plugins in the previous part about content negotiation. Ktor provides a number of plugins out of the box, as well as a simple API to implement your own plugins. However, Ktor doesn\u2019t prevent you from using any of your favorite libraries without a plugin. A plugin is just a convenience that enables common configuration and a coherent code style.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">install(ContentNegotiation) {\n   json()\n}\ninstall(WebSockets) {\n   pingPeriod = Duration.ofSeconds(15)\n   timeout = Duration.ofSeconds(15)\n   maxFrameSize = Long.MAX_VALUE\n   masking = false\n}\ninstall(Compression) {\n   gzip {\n       minimumSize(1024)\n   }\n}\ninstall(CORS) {\n   allowMethod(HttpMethod.Put)\n   allowHeader(\"MyCustomHeader\")\n}<\/pre>\n\n\n\n<p>The plugins can be enabled for the application instance or for the selected routes. The full list of plugins can be found in the <a href=\"https:\/\/start.ktor.io\/settings\" target=\"_blank\" rel=\"noopener\">project generator<\/a> or in the <a href=\"https:\/\/github.com\/ktorio\/ktor-plugin-registry\" target=\"_blank\" rel=\"noopener\">GitHub repository for the plugins registry<\/a>, where plugin developers can contribute by registering their plugins.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Extensibility through interceptors<\/h2>\n\n\n\n<p>Ktor provides a powerful interception mechanism that allows you to add custom behavior to your application. This offers flexibility similar to aspect-oriented programming, but in a more Kotlin-idiomatic way.<\/p>\n\n\n\n<p>To create an interceptor, we only need to create an instance of an application plugin using the <code>createApplicationPlugin<\/code> function. In the plugin, we can implement custom logic for handling requests and responses using a set of <a href=\"https:\/\/ktor.io\/docs\/server-custom-plugins.html#call-handling\" target=\"_blank\" rel=\"noopener\">handlers<\/a> that provide access to different stages of a call.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">val UserAgentValidation = createApplicationPlugin(\"UserAgent\") {\n   onCall { call ->\n       val userAgent = call.request.headers[\"User-Agent\"]\n    ?: throw UnsupportedUserAgentException()\n       if (userAgent.isNotBrowser())\n    call.respond(HttpStatusCode.Forbidden)\n   }\n}\n\ninstall(UserAgentValidation)<\/pre>\n\n\n\n<p>In our example, the <code>onCall<\/code> function implements the logic of checking the user agent header and rejecting any clients that aren\u2019t web browsers. Keep in mind, we still need to use the <code>install<\/code> function to activate the plugin.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Kotlin Coroutines<\/h2>\n\n\n\n<p>The Ktor project started as a testbed for <a href=\"https:\/\/kotlinlang.org\/docs\/coroutines-overview.html\" target=\"_blank\" rel=\"noopener\">Kotlin coroutines<\/a>. This is why it provides native support for asynchronous programming. This allows for more intuitive, sequential-looking code that&#8217;s actually non-blocking.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import io.ktor.server.application.*\nimport io.ktor.server.response.*\nimport io.ktor.server.routing.*\n\nfun Application.module() {\n    routing {\n        get(\"\/api\/users\") {\n     selectAllUsers().let {\n         call.respond(it)\n     }\n        }\n    }\n}\n\nsuspend fun selectAllUsers(): List&lt;User> =\n   newSuspendedTransaction(Dispatchers.IO) {\n       UserTable.selectAll()\n           .orderBy(UserTable.id, SortOrder.ASC)\n           .map { row: ResultRow ->\n               User(\n                   userId = row[UserTable.id],\n                   name = row[UserTable.name],\n                   \/\/ map more columns\n               )\n           }\n   }\n\nobject UserTable : Table(\"users\") {\n   val id = long(\"id\").autoIncrement()\n\n   val name = varchar(\"name\", 50).uniqueIndex()\n   val email = text(\"email\").uniqueIndex()\n   val link = text(\"link\").nullable()\n   val userType = enumeration&lt;UserType>(\"user_type\")\n\n   override val primaryKey: Table.PrimaryKey = PrimaryKey(id)\n}<\/pre>\n\n\n\n<p>In this example, <code>selectAllUsers<\/code> is marked with the <code>suspend<\/code> keyword, meaning that it can pause its execution without blocking the thread. In this example, we use the <a href=\"https:\/\/jetbrains.github.io\/Exposed\/home.html\" target=\"_blank\" rel=\"noopener\">Exposed<\/a> library for accessing the database and then mapping the results of the query to a <code>User<\/code> object.<\/p>\n\n\n\n<p>You will notice that the editor shows the special icons in the gutter, indicating that there is a suspending function invocation on the corresponding line:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"1948\" height=\"942\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/09\/ktor-async-invocations-1.png\" alt=\"\" class=\"wp-image-512645\"\/><\/figure>\n\n\n\n<p><em>On lines 17, 19, and 21, the icons in the gutter indicate invocations of the asynchronous functions.<\/em><\/p>\n\n\n\n<p>When working with Ktor, you will notice that even though the application code looks sequential, there are a lot of functions highlighted in the gutter with these icons. This is a hint for you that in those places, the execution is actually asynchronous.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">WebSockets<\/h2>\n\n\n\n<p>Thanks to coroutines, Ktor makes it easy to add real-time, bidirectional communication to your application using <a href=\"https:\/\/ktor.io\/docs\/server-websockets.html\" target=\"_blank\" rel=\"noopener\">WebSockets<\/a>. Here is an example of a very simple echo server with WebSockets:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">fun main() {\n   embeddedServer(Netty, port = 8080) {\n       install(WebSockets)\n       routing {\n           webSocket(\"\/chat\") {\n               send(\"You are connected!\")\n               for (frame in incoming) {\n                   frame as? Frame.Text ?: continue\n                   val receivedText = frame.readText()\n                   send(\"You said: $receivedText\")\n               }\n           }\n       }\n   }.start(wait = true)\n}<\/pre>\n\n\n\n<p>The server sets up a WebSocket endpoint at <code>\/chat<\/code> that echoes back messages prefixed with &#8220;You said: &#8220;.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Testability<\/h2>\n\n\n\n<p>Ktor was designed with testability in mind. It provides a test engine that allows you to test your application without starting a real server:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">import io.ktor.client.request.*\nimport io.ktor.client.statement.*\nimport io.ktor.http.*\nimport io.ktor.server.testing.*\nimport kotlin.test.*\n\nclass ApplicationTest {\n    @Test\n    fun testRoot() = testApplication {\n        application { \n            module()\n        }\n        client.get(\"\/\").apply {\n            assertEquals(HttpStatusCode.OK, status)\n            assertEquals(\"Hello, World!\", bodyAsText())\n        }\n    }\n}<\/pre>\n\n\n\n<p>The provided test code demonstrates how Ktor allows you to write tests for your application using its <a href=\"https:\/\/ktor.io\/docs\/server-testing.html\" target=\"_blank\" rel=\"noopener\">TestEngine<\/a>. The test simulates an HTTP request to the Ktor server without needing to start an actual server, which is both faster and easier to manage during development. This makes writing comprehensive tests for your Ktor applications a breeze, including full integration tests.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Authentication<\/h2>\n\n\n\n<p>Ktor provides a flexible authentication system that supports various authentication methods. Below is an example of basic authentication:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">embeddedServer(Netty, port = 8080, host = \"0.0.0.0\") {\n   install(Authentication) {\n       basic(\"auth-basic\") {\n           realm = \"Access to the '\/' path\"\n           validate { credentials ->\n               if (credentials.name == \"username\" \n                      &amp;&amp; credentials.password == \"password\") {\n                   UserIdPrincipal(credentials.name)\n               } else {\n                   null\n               }\n           }\n       }\n   }\n\n   routing {\n       authenticate(\"auth-basic\") {\n           get(\"\/\") {\n               call.respondText(\"Hello,\n                   ${call.principal&lt;UserIdPrincipal>()?.name}!\")\n           }\n       }\n   }\n\n}.start(wait = true)<\/pre>\n\n\n\n<p>The example demonstrates how to implement basic authentication in Ktor. When a client, such as a browser, requests the root path (&#8216;\/&#8217;) without providing an <code>Authorization<\/code> header, the server responds with a <code>401<\/code> (Unauthorized) status code. It also includes a <code>WWW-Authenticate<\/code> response header, signaling that the route is protected by basic authentication.<\/p>\n\n\n\n<p>Upon receiving this response, the client typically displays a login dialog for the user to enter their credentials. The client then makes a new request, this time including an <code>Authorization<\/code> header containing the Base64-encoded username and password pair.<\/p>\n\n\n\n<p>Once the server receives a properly authenticated request, it validates the credentials and allows access to the root endpoint (&#8216;\/&#8217;). In this example, the endpoint responds with a personalized message: &#8220;Hello, username&#8221;.<\/p>\n\n\n\n<p>Ktor also supports other authentication methods, including <a href=\"https:\/\/ktor.io\/docs\/server-form-based-auth.html\" target=\"_blank\" rel=\"noopener\">form-based<\/a>, <a href=\"https:\/\/ktor.io\/docs\/server-jwt.html\" target=\"_blank\" rel=\"noopener\">JWT<\/a>, and <a href=\"https:\/\/ktor.io\/docs\/server-oauth.html\" target=\"_blank\" rel=\"noopener\">OAuth<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Serving static content<\/h2>\n\n\n\n<p>Serving <a href=\"https:\/\/ktor.io\/docs\/server-static-content.html\" target=\"_blank\" rel=\"noopener\">static content<\/a> is an integral part of the routing functionality. Thus, an extra plugin is not required and the functionality is enabled by default. To configure the resource mapping, we can use Ktor\u2019s API functions, such as <code>staticResources<\/code> in the example below:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img decoding=\"async\" loading=\"lazy\" width=\"2110\" height=\"1250\" src=\"https:\/\/blog.jetbrains.com\/wp-content\/uploads\/2024\/09\/ktor-static-resources.png\" alt=\"\" class=\"wp-image-512668\"\/><\/figure>\n\n\n\n<p><em>The root path (\u2018\/\u2019) is mapped to the `static` folder in the project.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Status Pages<\/h2>\n\n\n\n<p>By using the <a href=\"https:\/\/ktor.io\/docs\/server-status-pages.html\" target=\"_blank\" rel=\"noopener\">status pages<\/a> plugin, we can configure how the application responds to exceptions and status codes.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"generic\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">install(StatusPages) {\n   exception&lt;AuthorizationException> { call, cause ->\n       call.respond(HttpStatusCode.Forbidden)\n   }\n   exception&lt;Throwable> { call, cause ->\n       call.respondText(text = \"500: ${cause.localizedMessage}\",\n           status = HttpStatusCode.InternalServerError)\n   }\n   status(HttpStatusCode.NotFound) { call, _ ->\n       call.respondRedirect(\"\/404.html\", permanent = true) \/\/ 301\n   }\n}<\/pre>\n\n\n\n<p>This setup provides custom responses for unauthorized access and any other unhandled exception that might occur during the execution. For the 404 status code, the request is redirected to a static resource, assuming the static resources are configured accordingly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>Ktor offers a fresh, Kotlin-first approach to building server-side applications. Its coroutine-based architecture, modular design, and type-safe API provide a powerful toolkit for modern application development. Ktor&#8217;s flexibility and performance make it an excellent choice for microservices, API backends, and other modern application architectures.<\/p>\n\n\n\n<p>For Java developers looking to leverage Kotlin&#8217;s strengths in their server-side applications, Ktor provides a compelling toolkit that&#8217;s worth exploring. Its learning curve is relatively gentle for those already familiar with Kotlin, and the benefits in terms of code conciseness and performance can be significant.<\/p>\n\n\n\n<p>This article provided a brief overview of Ktor\u2019s features. For more examples, we recommend the <a href=\"https:\/\/github.com\/ktorio\/ktor-samples\" target=\"_blank\" rel=\"noopener\">samples repository on GitHub<\/a> where we showcase various features provided out of the box.<\/p>\n\n\n\n<p>You can start working with Ktor right away by creating your project using the <a href=\"https:\/\/start.ktor.io\/settings\" target=\"_blank\" rel=\"noopener\">Ktor Generator<\/a>.<\/p>\n","protected":false},"author":820,"featured_media":512783,"comment_status":"closed","ping_status":"closed","template":"","categories":[808,4113,2347],"tags":[21,4292,288],"cross-post-tag":[],"acf":[],"_links":{"self":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/idea\/512597"}],"collection":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/idea"}],"about":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/types\/idea"}],"author":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/users\/820"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/comments?post=512597"}],"version-history":[{"count":9,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/idea\/512597\/revisions"}],"predecessor-version":[{"id":514299,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/idea\/512597\/revisions\/514299"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/media\/512783"}],"wp:attachment":[{"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/media?parent=512597"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/categories?post=512597"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/tags?post=512597"},{"taxonomy":"cross-post-tag","embeddable":true,"href":"https:\/\/blog.jetbrains.com\/fr\/wp-json\/wp\/v2\/cross-post-tag?post=512597"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}