A really simple library allowing Folder routing for Ktor.
- Contents
- Quickstart
- Configuration
- Routes
- Websockets
- Error handlers
- Named method routes
- Custom path
- Authentication
- Multi routes
- Name transforming
- Other info
Better Ktor (BK) is available on Maven central. The minimum version supported is Java 11.
<dependency>
<groupId>codes.rorak</groupId>
<artifactId>betterktor</artifactId>
<version>1.1.5</version>
</dependency>
implementation("codes.rorak:betterktor:1.1.5");
To quickly start with BK, just install the plugin in your main Ktor module:
import codes.rorak.betterktor.BKPlugin;
fun Application.module() {
install(BKPlugin);
}
Now just create a package called endpoints
and place this code into a file called Test.kt
:
class Test: BKRoute {
override suspend fun get(call: ApplicationCall) {
call.respondText("Hello, world!");
}
}
If you now run your app and go to ip:port/test
a message "Hello world" is shown.
install(BKPlugin) {
endpointsPackage = "endpoints";
basePackage = "com.example";
casing = BKTransform::kebabCase;
rootPath = "/api";
installWebSockets = true;
defaultNamedRouteMethod = BKHttpMethod.GET
configureAuthentication {
// ... auth config
}
};
endpointsPackage
- name of the package, where endpoints are storedbasePackage
- name of the package, where your files are - if null, BK will try to figure outcasing
- a method transforming the name of your file & package into the endpoint name. See casingrootPath
- base path for HTTP routes for all endpointsinstallWebSockets
- whether to install websockets inside BKPlugin if they are needed. Set this to false if you want to configure them yourselfdefaultNamedRouteMethod
- default HTTP method for Named route methodsconfigureAuthentication()
- a method/configuration of the Authentication plugin. You MUST configure it in BK, if you want to use BK Auth
Using the interface Route
you can easily create a route. The path of the route will be:
config.rootPath + package inside endpoints + name of the class
So a path for com.example.endpoints.user.SomeClass
with config.rootPath = "/api"
would be
/api/user/some-class
.
The interface provides all HTTP methods to override twice. You can choose to use the simple implementation
with just one parameter call
, or if you use call.request
a lot, you can also use an implementation with
two parameters - call
and request
!
Here is an example implementation:
class User: BKRoute {
override suspend fun get(call: ApplicationCall, request: ApplicationRequest) {
call.respondText(request.cookies["cookie"]);
}
override suspend fun post(call: ApplicationCall) {
call.respond(UserObject("Peter", 11));
}
}
BK also supports web socket routes. Using the interface BKWebsocket
you can easily create and manage a socket!
Paths work the same as with Routes, but the methods the interfaces provides are much simpler.
There is just one method in three overloads - handle
. This method requires a websocket session as it's first
parameter,
but if you want to also have the call
and the request
parameter - also possible:
suspend fun handle(session: DefaultWebSocketServerSession, call: ApplicationCall, request: ApplicationRequest) {};
suspend fun handle(session: DefaultWebSocketServerSession) {};
suspend fun handle(session: DefaultWebSocketServerSession, call: ApplicationCall) {};
Here is an example:
class User: BKWebsocket {
override suspend fun handle(session: DefaultWebSocketServerSession) {
session.send("Hello there!");
session.close(CloseReason(CloseReason.Codes.NORMAL), "Bye!");
}
}
And the last interface is BKErrorHandler
and it's the simplest one. You have just one method:
onError(call, cause)
. Paths work the same as with Routes, but the class name **is not added to the path!
**. (Exception Multi route)
Here is an example:
class UserErrorHandler: BKErrorHandler {
override suspend fun onError(call: ApplicationCall, cause: Throwable) {
call.respondText(cause.message ?: "Error!");
}
}
It might be a little annoying to create a new class for every route, so you have an option to create more routes in the class using normal methods. Start by creating a route like you would normally, maybe try also adding a normal GET method.
class User: BKRoute {
override suspend fun get(call: ApplicationCall) {
call.respondText("Hello user!");
}
}
Now create a new method and name it how you want the route be named.
Don't forget that the method name will be also transformed by config.casing
.
class User: BKRoute {
override suspend fun get(call: ApplicationCall) {
call.respondText("Hello user!");
}
override suspend fun findUser(call: ApplicationCall) {
call.respond(UserName("user_123", "User 123"));
}
}
For these named routes you can also use two parameters, as with normal method routes.
But if you try going to ip:port/user/find-user
, nothing is happening, why?
It's because all named routes are POST
by default, but this behaviour can be changed using:
- The
@BKGet
annotation for the method. Easy and simple! (@BKPost
also exists) - For other methods, such as PUT or DELETE, you can use the
@BKMethod(method)
annotation and choose a method there - To override this default behaviour for the class, use
@BKDefaultMethod(method)
(for the class) - You can change
config.defaultNamedRouteMethod
to override the default behaviour
Here is an example:
@BKDefaultMethod(BKHttpMethod.DELETE)
class User: BKRoute {
override suspend fun get(call: ApplicationCall) {
call.respondText("Hello user!");
}
@BKGet
override suspend fun findUser(call: ApplicationCall) {
call.respondText("GET /user/find-user");
}
@BKPost
override suspend fun editUser(call: ApplicationCall) {
call.respondText("POST /user/edit-user");
}
@BKMethod(BKHttpMethod.PUT)
override suspend fun newUser(call: ApplicationCall) {
call.respondText("PUT /user/new-user");
}
override suspend fun deleteUser(call: ApplicationCall) {
call.respondText("DELETE /user/delete-user");
}
}
Private methods will be ignored, but if you still want to ignore a method, use a @BKIgnore
annotation.
You can use this with Routes, Websockets or Error handlers.
Using an annotation @BKPath(path)
, you can set a custom path for your endpoint.
You can either set an absolute path, or a relative path.
An absolute path must start with /
and is, surprisingly, absolute. Not even config.rootPath
will be added.
A relative path will on the other hand replace just the class name in the path, so even the package stays in the path.
You can use any pattern features, like with routing (tailcard, wildcard...).
Here is an example:
@BKPath("user-{id}") // relative path
class User: BKRoute {
override suspend fun get(call: ApplicationCall) {
call.respond(call.parameters["id"]);
}
}
Regex path works exactly the same as a Custom path, but you can use regex.
The annotation is @BKRegexPath
(who would have guessed) with one parameter path
.
You don't need an example...
BK also supports authentication. Please read Ktor authentication first.
You just use the @BKAuth(providers..., strategy)
annotation, where name specifies the providers and the strategy.
It is possible to apply authentication to a class or a function.
To use authentication routes you MUST install the plugin through the config.
If you want a class to be a multiple of routes (for example a route and a websocket, or a route and an error handler),
you can use multi routes. Just inherit multiple interfaces and mark the class with the @BKMulti
annotation.
Inside the annotation's parameter you can choose what route type will use it's named methods, so for example if you
specify @BKMulti(BKRouteType.WEBSOCKETS)
, all named routes will automatically be in websockets. You can override
this behaviour with @BKMultiFor(type)
annotation, which you can apply to any named route method.
Important: Error handler in multi routes WILL include the class name, unlike normal error handler!
Here is an example:
@BKMulti // the type is BKRouteType.ROUTE by default
class User: BKRoute, BKWebsocket, BKErrorHandler {
override suspend fun get(call: ApplicationCall) {
call.respondText("GET http://ip:port/user");
}
override suspend fun handle(session: DefaultWebSocketServerSession) {
session.send("ws://ip:port/user BUT NOT POST http://ip:port/user/handle");
session.close();
}
override suspend fun onError(call: ApplicationCall, error: Throwable) {
call.respondText("Error handler for /user, but NOT ws://ip:port/user/on-error, NOR http://ip:port/user/on-error");
}
suspend fun new(call: ApplicationCall) {
call.respondText("POST http://ip:port/user/new");
}
@BKMultiFor(BKRouteType.WEBSOCKET)
suspend fun chat(session: DefaultWebSocketServerSession) {
session.send("ws://ip:port/user/chat");
session.close();
}
}
For transforming names BK provides an object - BKTransform
.
Inside you have 5 main casings methods - camelCase
, snake_case
, kebab-case
, PascalCase
and Train-Case
,
as well as a method String.toInternCase()
, which converts any casing into intern§casing
.
All the other methods use this intern casing to transform any name into specified casing.
For example, here is the implementation of camelCase
:
fun camelCase(s: String): String = "§([a-z])".toRegex().replace(s.toInternCase()) { it.groupValues[1].uppercase() };
You can expand this object using extension methods and the toInternCase
method.
This is how the program figures out the base package of the class, so if anything goes wrong, try setting
the basePackage
property:
for every $entry in the call stack:
if $entry.fullName starts with any of these:
$current-package, java.lang, io.ktor
skip it
else: return $entry.package