Fullstack
Declare a full stack project
In your site's build script, make sure you call configAsKobwebApplication(includeServer = true)
. Just with that done, you are ready to write server logic for your Kobweb site.
A Kobweb project will always at least have a JavaScript target, representing the frontend, but if you declare an intention to implement a server, that will create a JVM target for you as well. You can add dependencies to this target if you want to make them available to your server code:
The easy way to check if everything is set up correctly is to open your project inside IntelliJ IDEA, wait for it to finish indexing, and check that the jvmMain
folder is detected as a module (if so, it will be given a special icon and look the same as the jsMain
folder):
Define API routes
You can define and annotate methods which will generate server endpoints you can interact with. To add one:
- Define your method (optionally
suspend
able) in a file somewhere under theapi
package in yourjvmMain
source directory. - The method should take exactly one argument, an
ApiContext
. - Annotate it with
@Api
For example, here's a simple method that echoes back an argument passed into it:
After running your project, you can test the endpoint by visiting mysite.com/api/echo?message=hello
You can also trigger the endpoint in your frontend code by using the extension api
property added to the kotlinx.browser.window
class:
All the HTTP methods are supported (post
, put
, etc.).
These methods will throw an exception if the request fails for any reason. Note that for every HTTP method, there's a corresponding "try" version that will return null instead (tryPost
, tryPut
, etc.).
If you know what you're doing, you can of course always use window.fetch(...)
directly.
Respond to an API request
When you define an API route, you are expected to set a status code for the response, or otherwise it will default to status code 404
.
In other words, the following API route stub will return a 404:
In contrast, this minimal API route returns an OK status code:
The ctx.res.setBodyText
method sets the status code to 200 automatically for you, which is why code in an earlier section worked without setting the status directly. Of course, if you wanted to return a different status code value after setting the body text, you could explicitly set it right after making the setBodyText
call. For example:
The design for defaulting to 404 was chosen to allow you to conditionally handle API routes based on input conditions, where early aborts automatically result in the client getting an error.
A very common case is creating an API route that only handles POST requests:
Finally, note that you can add headers to your response. A common endpoint that some servers provide is a redirect (302) with an updated URL location. This would look like:
Simple!
Intercept API routes
Kobweb provides a way to intercept all incoming API requests, getting a first chance to handle them before they get passed to the actual API route handler.
To intercept all routes, declare a suspend method annotated with @ApiInterceptor
. This method must take a ApiInterceptorContext
parameter and return a Response
.
You can check the context parameter for the path associated with the incoming request. If you don't want to handle a particular path, you can call ctx.dispatcher.dispatch()
to pass it on as normal:
The ApiInterceptorContext
class provides access to a mutable version of the incoming request, which gives you a chance to modify it first (e.g. adding cookies, updating headers) before it gets handled, which can be useful.
The ctx.dispatcher.dispatch
method takes an optional path you can specify so that you can delegate a request to a different API route:
Perhaps you aren't interested in interfering with any incoming requests, but you want to modify all responses before they get sent back to the client. You can use this pattern for that:
API interceptors only work for API routes, not static files or other resources. In other words, although you can use an interceptor to intercept a call to "/api/users/edit", the feature is not designed to handle a user navigating to https://example.com/admin/dashboard
and then getting redirected to https://example.com/login
instead.
API interceptors also do not work for API streams (which will be discussed later in this article) since there's nothing general to intercept there. API streams connect once and then persist.
Dynamic API routes
Similar to Dynamic routes, you can define API routes using curly braces in the same way to indicate a dynamic value that should be captured with some binding name.
For example, the following endpoint will capture the value "123" into a key name called "article" when querying articles/123
:
Recall from the @Page
docs that specifying a name inside the curly braces defines the variable name used to capture the value. When empty, as above, Kobweb uses the filename to generate it. In other words, you could explicitly specify @Api("{article}")
in the above example for the exact same effect.
Once this API endpoint is defined, query it as you would any normal API endpoint:
@InitApi
methods and initializing services
A Kobweb server supports declaring methods that should be run when it starts up. These methods must be annotated with @InitApi
and must take a single InitApiContext
parameter.
If you are running a development server and change any of your backend code, causing a live reloading event, the init methods will be run again.
The InitApiContext
class exposes a mutable set property (called data
) which you can put anything into. Meanwhile, @Api
methods expose an immutable version of data
. This allows you to initialize a service in an @InitApi
method and then access it in your @Api
methods.
Let's demonstrate a concrete example. Imagine you have an interface called Database
and a mutable subclass MutableDatabase
that implements it and provides additional APIs for mutating the database.
The skeleton for registering and later querying such a database instance might look like this:
Define API streams
Kobweb servers support persistent connections via streams. Streams are essentially named channels that maintain continuous contact between the client and the server, allowing either to send messages to the other at any time. This is especially useful if you want your server to be able to communicate updates to your client without needing to poll.
Additionally, multiple clients can connect to the same stream. In this case, the server can choose to not only send a message back to your client, but also to broadcast messages to all users (or a filtered subset of users) on the same stream. You could use this, for example, to implement a chat server with rooms.
Example API stream
Like API routes, API streams must be defined under the api
package in your jvmMain
source directory. By default, the name of the stream will be derived from the file name and path that it is declared in (e.g. api/lobby/Chat.kt
will create a stream named "lobby/chat").
Unlike API routes, API streams are defined as properties, not methods. This is because API streams need to be a bit more flexible than routes, since streams consist of multiple distinct events: client connection, client messages, and client disconnection.
Also unlike API routes, streams do not have to be annotated. The Kobweb Application plugin can automatically detect them.
For example, here's a simple stream, declared on the backend, that echoes back any argument it receives:
To communicate with an API stream from your site, you need to create a stream connection on the client. We provide the rememberApiStream
method to help with this:
After running your project, you can click on the button and check the console logs. If everything is working properly, you should see "Echoed: hello!" each time you press the button.
Run kobweb create examples/chat
to instantiate a project that uses API streams to implement a very simple chat application. Feel free to reference that project for a more realistic example.
API stream conveniences
The above example was intentionally verbose, to showcase the broader functionality around API streams. However, depending on your use-case, you can elide a fair bit of boilerplate.
First of all, the connect and disconnect handlers are optional, so you can omit them if you don't need them. Let's simplify the echo example:
Additionally, if you only care about the text event, there are convenience methods for that:
In practice, your API streams will probably be a bit more involved than the echo example above, but it is nice to know that you can handle some cases only needing a one-liner on the server and another on the client to create a persistent client-server connection!
If you need to create an API stream with stricter control around when it actually connects to the server, you can create the ApiStream
object directly instead of using rememberApiStream
:
API routes vs. API streams
When faced with a choice, use API routes as often as you can. They are conceptually simpler, and you can query API endpoints with a CLI program like curl and sometimes even visit the URL directly in your browser. They are great for handling queries of or updates to server resources in response to user-driven actions (like visiting a page or clicking on a button). Every operation you perform returns a clear response code in addition to some payload information.
Meanwhile, API streams are very flexible and can be a natural choice to handle high-frequency communication. But they are also more complex. Unlike a simple request / response pattern, you are instead opting in to manage a potentially long lifetime during which you can receive any number of events. You may have to concern yourself about interactions between all the clients on the stream as well. API streams are fundamentally stateful.
You often need to make a lot of decisions when using API streams. What should you do if a client or server disconnects earlier than expected? How do you want to communicate to the client that their last action succeeded or failed (and you need to be clear about exactly which action because they might have sent another one in the meantime)? What structure do you want to enforce, if any, between a client and server connection where both sides can send messages to each other at any time?
Most importantly, API streams may not horizontally scale as well as API routes. At some point, you may find yourself in a situation where a new web server is spun up to handle some intense load.
If you're using API routes, you're already probably delegating to a database service as your data backend, so this may just work seamlessly.
But for API streams, you many naturally find yourself writing a bunch of broadcasting code. However, this only works to communicate between all clients that are connected to the same server. Two clients connected to the same stream on different servers are effectively in different, disconnected worlds.
The above situation is often handled by using a pubsub service (like Redis). This feels somewhat equivalent to using a database as a service in the API route situation, but this code might not be as straightforward to migrate.
API routes and API streams are not a you-must-use-one-or-the-other situation. Your project can use both! In general, try to imagine the case where a new server might get spun up, and design your code to handle that situation gracefully. API routes are generally safe to use, so use them often.
However, if you have a situation where you need to communicate events in real-time, especially situations where you want your client to be continuously directed what to do by the server via events, API streams are a great choice.
You can also search online about REST vs WebSockets, as these are the technologies that API routes and API streams are implemented with. Any discussions about them should apply here as well.
Server logs
When you run kobweb run
, the spun-up web server will, by default, log to the .kobweb/server/logs
directory.
You can generate logs using the ctx.logger
property provided to @Api
calls.
You can configure logging behavior by editing the .kobweb/conf.yaml
file. Below we show setting all parameters to their default values:
The above defaults were chosen to be reasonable for most users running their projects on their local machines in developer mode. However, for production servers, you may want to set clearLogsOnStart
to false, bump up the totalSizeCap
after reviewing the disk limitations of your web server host, and maybe set maxFileCount
to a reasonable limit.
Most users might assume "10MB" is 10 * 1024 * 1024 bytes, but here it will actually result in 10 * 1000 * 1000 bytes. You probably want to use "KiB", "MiB", or "GiB" when you configure this value.
CORS
CORS, or Cross-Origin Resource Sharing, is a security feature built on the idea that a web page should not be able to make requests for resources from a server that is not the same as the one that served the page unless it was served from a trusted domain.
To configure CORS for a Kobweb backend, Kobweb's .kobweb/conf.yaml
file allows you to declare such trusted domains using a cors
block:
Specifying the schemes is optional. If you don't specify them, Kobweb defaults to "http" and "https".
You can also specify subdomains, e.g.
which would add CORS support for en.example.com
, de.example.com
, and es.example.com
, as well as example.com
itself.
Once configured, your Kobweb server will be able to respond to data requests from any of the specified hosts.
If you find that your full-stack site, which was working locally during development, rejects requests in the production version, check your browser's console logs. If you see errors in there about a violated CORS policy, that means you didn't configure CORS correctly.