diff --git a/server/README.md b/server/README.md
index 157e3f3..17ae9fd 100644
--- a/server/README.md
+++ b/server/README.md
@@ -5,3 +5,42 @@ Implement a Ktor API described with type-safety in common code.
+
+Spine allows us to [declare Ktor endpoints in code shared between the client and server](https://spine.opensavvy.dev/setup.html).
+
+## Declaring a fullstack endpoint
+
+First, create a module that will contain the endpoint definition (see the `api` module), with the following API:
+
+```kotlin
+object Api : RootResource("v1") {
+ object Users : StaticResource("users", Api) {
+ val create by post()
+ .request()
+ .response()
+ }
+}
+```
+
+Create a new module, with a dependency on `dev.opensavvy.spine:server`. Instantiate a Ktor server by following [our tutorial](https://spine.opensavvy.dev/setup.html#server-side-implementation). You can now declare the endpoint in [your existing `routing` block](https://ktor.io/docs/server-routing.html):
+
+```kotlin
+routing {
+ route(Api.Users.create) {
+ // The variable 'body' is automatically created with the correct type
+ println("Creating a user: ${body.username}")
+
+ // The 'respond' method expects the correct response type
+ respond(User(username = body.username))
+ }
+}
+```
+
+## Learn more
+
+- [`route`][opensavvy.spine.server.route] declares an endpoint to Ktor.
+- [`body`][opensavvy.spine.server.TypedResponseScope.body] deserializes the request body type-safely.
+- [`parameters`][opensavvy.spine.server.TypedResponseScope.parameters] provides access to query parameters.
+- [`idOf`][opensavvy.spine.server.TypedResponseScope.idOf] provides access to path parameters.
+- [`respond`][opensavvy.spine.server.respond] serializes the response type-safely.
+- [`fail`][opensavvy.spine.server.fail] responds type-safely with one of the declared failures.
diff --git a/server/src/commonMain/kotlin/Server.kt b/server/src/commonMain/kotlin/Server.kt
index 722a29a..b9fd62a 100644
--- a/server/src/commonMain/kotlin/Server.kt
+++ b/server/src/commonMain/kotlin/Server.kt
@@ -7,6 +7,23 @@ import io.ktor.server.routing.*
import io.ktor.utils.io.*
import opensavvy.spine.api.*
+/**
+ * Declares a Ktor handler matching a Spine [endpoint][opensavvy.spine.api.AnyEndpoint].
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.logIn) {
+ * val (user, token) = authService.verifyLogIn(body.username, body.password)
+ * call.response.cookies.append("token", token)
+ * respond(user)
+ * }
+ * }
+ * ```
+ *
+ * For the full list of available methods, see [TypedResponseScope].
+ */
@KtorDsl
inline fun Route.route(
endpoint: Endpoint,
diff --git a/server/src/commonMain/kotlin/TypedResponseScope.kt b/server/src/commonMain/kotlin/TypedResponseScope.kt
index 8a3f995..13e953d 100644
--- a/server/src/commonMain/kotlin/TypedResponseScope.kt
+++ b/server/src/commonMain/kotlin/TypedResponseScope.kt
@@ -14,14 +14,72 @@ import opensavvy.spine.api.FailureSpec.Or
import opensavvy.spine.api.Parameters
import kotlin.jvm.JvmName
+/**
+ * The various methods available within the handler of a Ktor endpoint.
+ *
+ * Full Ktor information is available via [call], as is standard in Ktor endpoints.
+ */
@KtorDsl
interface TypedResponseScope {
+
+ /**
+ * The standard Ktor [ApplicationCall] instance, which is used to access cookies
+ * or any other information directly from Ktor.
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.logIn) {
+ * // …perform log in…
+ *
+ * call.response.cookies.append("my-auth", "123")
+ * respond(Unit)
+ * }
+ * }
+ * ```
+ */
val call: ApplicationCall
+ /**
+ * The declared Spine [endpoint][opensavvy.spine.api.AnyEndpoint] which was called by the user.
+ */
val endpoint: Endpoint
+ /**
+ * The request body sent by the client.
+ *
+ * Spine automatically deserializes this value based on the [request][opensavvy.spine.api.AnyEndpoint.Builder.request] type declared in the endpoint.
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.logIn) {
+ * println("User ${body.username} wants to log in…")
+ * // …
+ * }
+ * }
+ * ```
+ */
val body: In
+ /**
+ * The parameters sent by the client.
+ *
+ * Spine automatically deserializes this value based on the [parameters][opensavvy.spine.api.AnyEndpoint.Builder.parameters] type declared in the endpoint.
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.list) {
+ * println("Include archived users? ${parameters.includeArchived}")
+ * // …
+ * }
+ * }
+ * ```
+ */
val parameters: Params
/**
@@ -49,16 +107,79 @@ interface TypedResponseScope TypedResponseScope<*, Out, *, *>.respond(body: Out, code: HttpStatusCode = if (body == Unit) HttpStatusCode.NoContent else HttpStatusCode.OK) {
call.respond(status = code, message = body)
}
+/**
+ * Responds with the given [body].
+ *
+ * This method is identical to [ApplicationCall.respond] but verifies that the [body] type matches the one declared in the endpoint.
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.logIn) {
+ * val user = authService.verifyLogIn(body.username, body.password)
+ * respond(user)
+ * }
+ * }
+ * ```
+ *
+ * ### Parameters
+ *
+ * - `body`: a value of the response type declared in the endpoint.
+ * If the endpoint declared a response type of [Unit], or declared no response at all, this parameter is optional.
+ * - `code`: the HTTP status code to respond with.
+ * Defaults to [HttpStatusCode.NoContent] if the response type is [Unit] or if no response type is declared.
+ * Defaults to [HttpStatusCode.OK] for any other value.
+ */
@KtorDsl
suspend fun TypedResponseScope<*, Unit, *, *>.respond(code: HttpStatusCode = HttpStatusCode.NoContent) {
respond(Unit, code)
}
+/**
+ * Fails the endpoint call with one of the declared [failures][opensavvy.spine.api.AnyEndpoint.Builder.failure].
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.logIn) {
+ * if (body.password.isBlank()) {
+ * fail(InvalidPassword)
+ * }
+ * }
+ * }
+ * ```
+ */
@KtorDsl
@JvmName("fail1")
suspend inline fun TypedResponseScope<*, *, Or<*, FailureSpec.ByCode>, *>.fail(failure: F): Nothing {
@@ -67,6 +188,21 @@ suspend inline fun TypedResponseScope<*, *, Or<*, FailureSpec.
throw SpineShortCircuitException()
}
+/**
+ * Fails the endpoint call with one of the declared [failures][opensavvy.spine.api.AnyEndpoint.Builder.failure].
+ *
+ * ### Example
+ *
+ * ```kotlin
+ * routing {
+ * route(Api.Users.logIn) {
+ * if (body.password.isBlank()) {
+ * fail(InvalidPassword)
+ * }
+ * }
+ * }
+ * ```
+ */
@KtorDsl
@JvmName("fail2")
suspend inline fun TypedResponseScope<*, *, Or>, Nothing>, *>.fail(failure: F): Nothing {
@@ -75,6 +211,21 @@ suspend inline fun TypedResponseScope<*, *, Or TypedResponseScope<*, *, Or>, Nothing>, Nothing>, *>.fail(failure: F): Nothing {
@@ -83,6 +234,21 @@ suspend inline fun TypedResponseScope<*, *, Or TypedResponseScope<*, *, Or>, Nothing>, Nothing>, Nothing>, Nothing>.fail(failure: F): Nothing {
@@ -91,6 +257,21 @@ suspend inline fun TypedResponseScope<*, *, Or TypedResponseScope<*, *, Or>, Nothing>, Nothing>, Nothing>, Nothing>, Nothing>.fail(failure: F): Nothing {
@@ -99,6 +280,21 @@ suspend inline fun TypedResponseScope<*, *, Or TypedResponseScope<*, *, Or>, Nothing>, Nothing>, Nothing>, Nothing>, Nothing>, Nothing>.fail(failure: F): Nothing {