Nomi
Compile-time REST for Kotlin Multiplatform — declare your API once, get both sides.
Nomi turns plain Kotlin interfaces into a working REST API.
You declare resources — nested interfaces that are your routes — and services — interfaces whose suspend functions are your endpoints.
A KSP processor generates a ktor server implementation from them; another generates a typed ktor client.
@Endpoint.Resource
interface Articles {
@Endpoint.Resource interface New : Articles
}
@Endpoint.Resource
interface Article {
val articleId: Uuid
}
@Endpoint.Service
interface ArticlesService : EndpointService {
suspend fun Articles.get(@Endpoint.Query page: Int? = null): Either<List<ArticleSummary>?, NoFail>
@Endpoint(Endpoint.Method.Post)
suspend fun Articles.New.create(content: ArticleContent): Either<Uuid, ArticleCreationFailure>
suspend fun Article.get(): Either<ArticleContent, NoFail>
}
@ClientEndpointServiceFactory(ArticlesService::class, "articles")
object APIServices
// Thanks to @ClientEndpointServiceFactory
val result = APIServices.articles(client).call {
Articles().New().create(ArticleContent(title = "Foo", author = "Me", content = "Bar"))
}
No reflection, no runtime route table, no hand-written URLs on either side.
Why Nomi
- One declaration, both sides
-
The resource and service interfaces in
commonMainare the only place the API is written down. The server’s routes and the client’s implementation are both generated from them, so a server and a client built against the same declaration can never disagree about a path, a method, or a type. - A route is a type, not a string
-
Nesting
@Endpoint.Resourceinterfaces is the path structure —Article.Commentsis a route because it is declared insideArticle. A path parameter is avalon the interface that owns it, not a placeholder you match against a string later. - Business failures are in the return type
-
Every endpoint returns
Either<V, E>. The server answers a failure withEither.error(…); the client receives it back asCallStatus.CallFailure(error), typed as the very sameE. No exception crosses the wire for a business outcome — only for things that are actually exceptional. - Everything is checked before anything is generated
-
A processor validates every declaration it reads — including ones from a dependency module — and generates nothing at all for a round in which anything it needs is invalid. There is no partial output and no runtime "route not found".
- The API documents itself
-
Set
withOpenApiSpecsand the generated routes describe themselves, so ktor serves an OpenAPI document straight from the routing tree. - Serialization stays yours
-
Nomi defines the shape of a call — method, path, query parameters, status code, whether a body is present — and nothing about the format of that body. It hands the value to ktor’s
ContentNegotiationand lets it serialize. - Multiplatform client, JVM server
-
Resources and services live in a
commonMainshared between every target. The generated client runs on all of them; the generated server runs on ktor-server, which is JVM-only.
How to read this documentation
-
Getting started gets a first call working in a few minutes.
-
The Core section explains resources, services,
Either, stringers, multipart and the wire protocol, which both sides share. -
The Server and Client sections cover each generated side.
-
The Guides section covers the demo, the supported platforms and visibility.