Concepts: Controllers (Core)
In this section we shall discuss the concept of a controller within the Sodacore framework.
Introduction
A controller is a class (or collection) of methods that will handle an incoming event, and dispatch a response.
Think of a controller as a location for your business logic, now when I say business logic, I don't mean that you should put all your code in here, but rather the logic that is specific to the request being handled, Providers are a great place to put reusable logic that can be used across multiple controllers or even just normal classes, but in general, think of a controller that follows the MVC pattern, where the controller essentially is your entry and exit point.
Within the core package alone, the only concept is a controller for threads, however in other plugins, like HTTP, WebSockets and more, the concept stays the same, but the request and response type changes.
CAUTION
As of right now, the controllers for threads aren't documented, this is because the API is going to change and the whole threads concept will re-evaluated, as workers are the same thing, but better.
HTTP Controllers
Within the HTTP plugin, controllers, noted above, are collections of methods that handle incoming HTTP responses and return responses.
Controllers are decorated with the @Controller decorator, which takes a base path as an argument, this base path is then prepended to all routes within the controller.
import { Controller, Get } from '@sodacore/http';
@Controller('/users')
export class UserController {
@Get('/')
async getUsers() {
// ...
}
}In the above example, we have a controller that handles requests to the /users endpoint, and within that controller, we have a method that handles GET requests to the / endpoint, which in this case would be /users.
As of right now, the HTTP package implements the following method decorators:
@Get@Post@Put@Delete@Patch@Options
Each of these decorators takes a path as an argument, which is then appended to the base path of the controller.
Paths can be static, or they can be dynamic, using the : syntax, and additionally, a path can be optional by suffixing a ? to the end of the path segment.
import { Controller, Get } from '@sodacore/http';
@Controller('/users')
export class UserController {
@Get('/:id')
public async getUser(@Params('id') id: string) {
// ...
}
@Get('/logout/:session?')
public async logout(@Params('session') session?: string) {
// ...
}
}TIP
You can use various pattern matching techniques, like dynamic segments, optional segments, and glob-style segments, read more.
Useful Notes
As the Sodacore framework wants to try and avoid as much useless boilerplate as possible, the methods you write have some useful defaults when returning data.
When you return a value we will automatically convert it to a Response object for you, using the following rules:
- If
Response, returns as-is, so you still have full control. - If
Error, returns a 500 response with the error message. - If
null, returns a 404 response. - If
stringornumber, returns a 200 response with the value as the body. - If
object, returns a 200 response with a JSON body. - If
true, returns a 201 response. - If
false, returns a 400 response. - If
undefined, returns a 204 response.
WebSocket Controllers
Within the WebSocket plugin, controllers are collections of methods that handle incoming WebSocket messages and may return responses.
Controllers are decorated with the same @Controller decorator, but instead are imported from the @sodacore/ws package and take a namespace string as an argument, this namespace is then prepended to all events within the controller. All methods are then exposed using the @Expose decorator.
TIP
In the WebSocket plugin, the way routing works is you send a structured JSON object that contains a command which is <namespace>:<method> and a context object, which contains any data you want to send to the server.
import { Controller, Expose, WsContext } from '@sodacore/ws';
@Controller('chat')
export class ChatController {
@Expose()
public async message(ctx: WsContext) {
// Handle incoming WebSocket message.
}
}As you can see we define a controller, with a namespace of chat, we then expose a method called message, which will handle incoming WebSocket messages with the command chat:message, WsContext is a helper class that allows you to easily access the underlying WebSocket connection, as well as any data that was sent with the message.
Useful notes
- All methods must be decorated with
@Exposeto be registered. - Any data returned will be sent back to the client, in the exact same format.
Worker Controllers
CAUTION
Worker controllers do work, but they have quite a few gotchas.
Worker controllers are the exact same as normal HTTP controllers, however, you can wrap them in a @Worker(...) decorator, which will then run the controller in a separate worker instance. I have tested this, and they do work quite nicely, BUT be aware there are some gotchas, see below.
- As workers are different contexts, remember that injected services won't work, as the worker is a different context, so you will need to manually instantiate any services you need.
- You cannot share memory between the main thread and the worker, so any data you need to share, you will need to serialize and send over, or use a shared database or cache.
- Expanding on the above point, if you tried to use
@Query()to get theURLSearchParamsobject, this will throw an error, because theURLSearchParamsobject is not serializable, so you will need to use@Params('someKey')to get specific primitive values only. - Returning data will be a similar issue, you wouldn't be able to return a
Responseobject either.
I definitely intend to support this feature as best as possible, potentially changing the decorators that can return whole objects, to return only objects, and not Maps, Sets, etc, but for the time being, be aware of the limitations.
