| src/katalis | ||
| .gitignore | ||
| katalis.nimble | ||
| LICENSE | ||
| README.md | ||
Gold Sponsor
|
|
Katalis
Katalis is nim lang micro web framework
Katalis always focusing on protocol implementation and performance improvement. For fullstack framework using katalis it will be depends on developer needs, we will not provides frontend engine or database layer engine (ORM) because it will vary for each developer taste!.
If you want to use katalis as fullstack nim, you can read on fullstack section in this documentation.
Do you think this is good project? support us for better development and support
- USDT (TRC20): TSGAgbb3fVdJfjHagDWhSySojo6bK89LMN
- USDT (BEP20): 0x26772823bdd8db6fbd010c1b15a1ba7496ce76fe
- Paypal : paypal.me/amrurosyada
Table of Contents
- Install
- Running simple app
- Katalis DSL (Domain Specific Language)
- Configuration
- Serve static file
- Create routes and handling request
- Query string, form (urlencoded/multipart), json, xml, upload, Redirect, Session
- Before, After, Middleware, OnReply, Cleanup Pipelines
- Response message
- Validation
- Template engine (Mustache)
- Websocket
- SSE (Server Sent Event)
- Serve SSL
- Deploy to shared hosting that support Common Gateway Interface CGI
- Katalis as fullstack
- Katalis coding style guideline
- Katalis structure
- Enable trace for debugging and logging
- AI generated Docs
1. Install
nimble install katalis
If some reason failed to install using nimble directory, you can install directly from the github
nimble install https://github.com/zendbit/katalis
2. Running simple app
After installing katalis, you can simply create your first app by import katalisApp
import katalis/katalisApp
Katalis has some macros for it's own DSL. Katalis use @! as macros prefix (We will explore about katalis DSL on the next section).
Create directory for the app
mkdir simpleapp
cd simpleapp
Create nim source file, in this case we will use app.nim
import katalis/katalisApp
## lets do some basics configuration
@!Settings.enableServeStatic = true
@!Settings.enableKeepAlive = true
## we also can use custom port
#@!Settings.port = Port(8080)
## lets start simple app
@!App:
@!Get "/":
@!Context.reply(Http200, "<h1>Hello World!.</h1>")
## kick and start the app
@!Emit
Compile the source with --threads:on switch to enable thread support and run it!.
nim c -r --threads:on app.nim
Katalis also support for chronos async/await framework https://github.com/status-im/nim-chronos as asyncdispatch backend
nim c -r --thread:on -d:asyncBackend=chronos app.nim
Katalis will run on port 8000 as default port
Listening non secure (plain) on http://0.0.0.0:8000
Lets open with the browser http://localhost:8000
3. Katalis DSL (Domain Specific Language)
Katalis come with Domain Specific Language, the purpose using DSL is for simplify the development and write less code. Katalis using @! prefix for the DSL to prevent confict and make it easy for coding convention. Katalis DSL available in katalis/macros/sugar.nim. There are some macros that only can be called inside @!App block and block pipeline in katalis let see the table.
Available on outside @!App block
| Name | Description |
|---|---|
| @!Settings | katalis settings instance, related to Settings type object in katalis/core/environment.nim |
| @!Emit | start katalis app, related to Katalis type object in katalis/core/katalis.nim |
| @!Routes | katalis routes object instance, related to Route type object in katalis/core/routes.nim |
| @!Katalis | katalis object instance, related to Katalis type object in katalis/core/katalis.nim |
| @!Environment | katalis environment instance, related to Environment type object in katalis/core/environment.nim |
| @!SharedEnv | katalis shared Table[string, string] type object for sharing between the app instance, related to Environment type object in katalis/core/environment.nim |
| @!Trace | trace block for displaying debug message, available when @!Settings.enableTrace = true |
Available only inside @!App block
Route pipeline
| Name | Description |
|---|---|
| @!Before | before route block pipeline |
| @!After | after route block pipeline |
| @!OnReply | on reply pipeline |
| @!Cleanup | cleanup pipeline |
| @!EndPoint | set endpoint for each route prefix (Optional) |
| @![Get, Post, Patch, Delete, Put, Options, Trace, Head, Connect] | http method for routing |
Available only inside @!App block
HttpContext and Environment
| Name | Description |
|---|---|
| @!Context | http context route parameter, related to HttpContext type object in katalis/core/httpContext.nim |
| @!Env | environment route parameter, related to Environment type object in katalis/core/environment.nim |
| @!Req | request context from client, related to Request type object in katalis/core/request.nim |
| @!Res | response context to client, related to Response type object in katalis/core/response.nim |
| @!WebSocket | websocket context service, related to WebSocket type object in katalis/core/webSocket.nim |
| @!Client | socket client context, related to AsyncSocket type object in katalis/core/httpContext.nim |
| @!Body | request body from client, related to Request type object field in katalis/core/request.nim |
| @!Segment | path segment url from client request, related to RequestParam type object in katalis/core/request.nim also related to Table[string, string] nim stdlib |
| @!Query | query string from client request, related to RequestParam type object in katalis/core/request.nim also related to Table[string, string] nim stdlib |
| @!Json | json data from client request, related to RequestParam type object in katalis/core/request.nim also related to JsonNode nim stdlib |
| @!Xml | xml data from client request, related to RequestParam type object in katalis/core/request.nim also related to XmlNode nim stdlib |
| @!Form | form data from client will handle form urlencode/multipart, related to Form type object in katalis/core/form.nim |
DSL Code structure in Katalis
## available on global
## @!Settings
## @!Emit -> Should called after @!App block
## @!Route
## @!Katalis
## @!Environment
## @!SharedEnv
## @!Trace
## katalis app block
@!App:
## code here
## endpoint optional, this endpoint prefix path will append to each route path request
## this is optional, and should be define before all other pipeline
@!EndPoint "/test/api"
## before route block
@!Before:
## available here
## @!Context
## @!Req
## @!Res
## @!Env
## @!Res
## @!WebSocket
## @!Client
## @!Body
## @!Segment
## @!Query
## @!Json
## @!Xml
## @!Form
## also global katalis macros
## code here
## after route block
@!After:
## code here
## on reply block
@!OnReply:
## code here
## cleanup block
@!Cleanup:
## code here
## routing
## available method @!Get, @!Post, @!Put, @!Delete, @!Patch, @!Head, @!Connect, @!Options, @!Trace
@!Post "/register":
## code here
@!Get "/home":
## code here
## also support for multiple method on routing
@![Get, Post] "/login":
## code here
4. Configuration
Configuration can be set using @!Settings macro. See katalis/core/environment.nim (Settings object type)
@!Settings.address = "0.0.0.0" ## default
@!Settings.port = Port(8000) ## default
## available settings (default value, all size metric in bytes):
## address: string = "0.0.0.0"
## port: Port = Port(8000)
## enableReuseAddress: bool = true
## enableReusePort:bool = true
## sslSettings: SslSettings = nil
## maxRecvSize: int64 = 104857600
## enableKeepAlive: bool = true
## enableOOBInline: bool = false
## enableBroadcast: bool = false
## enableDontRoute: bool = false
## storagesDir: string = getCurrentDir()/"storages".Path
## storagesUploadDir: string = getCurrentDir()/"storages".Path/"upload".Path
## storagesBodyDir: string = getCurrentDir()/"storages".Path/"body".Path
## storagesSessionDir: string = getCurrentDir()/"storages".Path/"session".Path
## staticDir: string = getCurrentDir()/"static".Path
## enableServeStatic: bool = false
## chunkSize: int = 8129
## readRecvBuffer: int = 32768
## enableTrace: bool = false
## maxSendSize: int = 104857600
## enableRanges: bool = true
## rangesSize: int = 32768
## enableCompression: bool = true
5. Serve static file
Serving static file size default value is arround 100M max, if you want to increase max send file size value you can increase @!Settings.maxSendSize. But be mindfull, make sure you have good server resource to handle it.
Serve individual file with app route
@!App:
@!Get "/my-picture-profile":
@!Context.replySendFile("my-profile-image.png".Path)
@!Get "/my-funny-video":
@!Context.replySendFile("funny-video.mp4".Path)
Serve plugin static file
For serving static file like static html, css, image, video, etc. We only need to enable enableServeStatic in katalis settings.
Lets create serverstatic-example folder.
mkdir servestatic-example
cd servestatic-example
Then create static folder inside servestatic-example folder
mkdir static
Inside servestatic-example folder we create minimal katalis app for serving static file. In this case we create app.nim
import katalis/katalisApp
## enable static file service
@!Settings.enableServeStatic = true
@!Settings.enableKeepAlive = true
@!Emit
Compile and start the server
nim c -r app.nim
Don't forget to put your static files into static folder
Access to static file should not include root of static dir in this case "static" dir:
For example want to access under css folder, we can access it using http://localhost:8000/css/somestyle.css
Open with browser http://localhost:8000/index.html
6. Create routes and handling request
import katalis/katalisApp
import katalis/plugins/nimMustache
@!Settings.enableServeStatic = true
@!Settings.enableKeepAlive = true
@!App:
## we can also create prefix for all routes
@!EndPoint "/admin" ## \
## all routes will prefixed with /admin/
## get request for default /
## in this case, because we already set @!EndPoint to "/admin"
## so the route url will be http://localhost:8000/admin
@!Get "/":
await @!Context.reply(Http200, "<h1>This is admin the root page!")
## another get example
## http://localhost:8000/hello
@!Get "/hello":
await @!Context.reply(Http200, "<h1>world!</h1>")
## mapping route, retrieve segment to variable
@!Get "/birthdate/:month/:day/:year":
## this will retrieve segments
## as variable month, day, and year
## http://localhost/admin/birthdate/may/22/2000
let birthdate = [@!Segment["month"], @!Segment["day"], @!Segment["year"]].join("/")
await @!Context.reply(
Http200,
&"<h3>Birthdate</h3> <p>{birthdate}</p>"
)
## mapping route, retrieve query string to variable
@!Get "/birthdate":
## this will retrieve query string
## as variable month, day, and year
## http://localhost/admin/birthdate?month=may&day=22&year=2000
let birthdate = [@!Query.getOrDefault("month"), @!Query.getOrDefault("day"), @!Query.getOrDefault("year")].join("/")
await @!Context.reply(
Http200,
&"<h3>Birthdate</h3> <p>{birthdate}</p>"
)
## mapping route, retrieve segment as regex pattern
@!Get "/birthdate/re<:month([a-zA-Z]+)_:day([0-9]+)_:year([0-9]+)>":
## this will retrieve query string
## as variable month, day, and year
## http://localhost/admin/birthdate/may_22_2000
let birthdate = [@!Segment.getOrDefault("month"), @!Segment.getOrDefault("day"), @!Segment.getOrDefault("year")].join("/")
await @!Context.reply(
Http200,
&"<h3>Birthdate</h3> <p>{birthdate}</p>"
)
## we also can have multiple method
## in one route definition
@![Get, Post] "/login":
## we can use validation to check fields
## but for validation details we will discuss in other section
## about validation
## let get form data username, password
## if method http post validate the form data
let username = @!Form.data.getOrDefault("username")
let password = @!Form.data.getOrDefault("password")
var errorMsg = ""
if @!Req.isHttpPost:
if username == "" or password == "":
errorMsg = "username or password is required!"
##
## katalis come with mustache template engine
## for template engine we will explain later
##
let tpl =
"""
<html>
<head>
<title>example</title>
</head>
<body>
<h3>Login</h3>
<form method="POST">
<input type="text" name="username" placeholder="username" value="{{username}}">
<br>
<input type="password" name="password" placeholder="password" value="{{password}}">
<br>
<input type="submit" value="Login">
</form>
<h4>{{errorMsg}}</h4>
</body>
</html>
"""
let m = newMustache()
m.data["errorMsg"] = errorMsg
m.data["username"] = username
m.data["password"] = password
@!Context.reply(Http200, m.render(tpl))
## we can have multiple @!App for routes separation
@!App:
## let definde endpoint for users
@!EndPoint "/user"
## the user root page
## http://localhost:8000/user
@!Get "/":
await @!Context.reply(Http200, "<h1>This is user root page</h1>")
@!Emit
we can also define route handler outside the @!App route and call it from the route definition, sometimes we want to split the logic from the route to make the code manageable
proc testHandler(ctx: HttpContext) {.async.} =
## ctx == @!Context
await @!Context.reply(Http200, "hello")
## if you want to pass environment to handler you can add more option to it
proc testHandler(ctx: HttpContext, env: Environment) {.async.} =
## ctx == @!Context
## env == @!Env
await @!Context.reply(Http200, "handler with env")
## pass other param is also straight forward
proc testHandler(ctx: HttpContext, myCarsBrand: seq[string]) {.async.} =
## ctx == @!Context
await @!Context.reply(Http200, "handler with custom param")
@!App:
@!Get "/test-handler":
await @!Context.testHandler
@!Get "/test-handler-1":
await @!Context.testHandler(@!Env)
@!Get "/test-handler-2":
await @!Context.testHandler(@["Toyota", "Ferrari", "Ford"])
7. Query string, form (urlencoded/multipart), json, xml, upload, Redirect, Session
7.1 Handling query string request
import katalis/katalisApp
@!Settings.enableServeStatic = true
@!Settings.enableKeepAlive = true
@!App:
@!Get "/test-qs":
## lets do query string test
## http://localhost:8000/test-qs?city=ngawi&province=surabaya with get method
let city = @!Query.getOrDefault("city")
let province = @!Query.getOrDefault("province")
@!Context.reply(Http200, &"<h3>Welcome to {province}, {city}.</h3>")
7.2 Handling form data
import katalis/katalisApp
@!Settings.enableServeStatic = true
@!Settings.enableKeepAlive = true
@!App:
@!Post "/test-form":
## lets do form test
## http://localhost:8000/test-form with post method
let city = @!Form.data.getOrDefault("city")
let province = @!Form.data.getOrDefault("province")
@!Context.reply(Http200, &"<h3>Welcome to {province}, {city}.</h3>")
7.3 Handling JSON data
All json request data will convert to nim stdlib json see https://nim-lang.org/docs/json.html
import katalis/katalisApp
@!Settings.enableServeStatic = true
@!Settings.enableKeepAlive = true
@!App:
@!Post "/test-json":
## lets do json test
## http://localhost:8000/test-json with post method
let data = @!Json ## \
## json data from client request
## all data will convert to nim stdlib JsonNode
## see https://nim-lang.org/docs/json.html
## lets modify the data add country to json
data["country"] = %"indonesia"
## katalis will automatic response as json if we pass JsonNode
## lets pass JsonNode from client and we modify it
await @!Context.replyJson(Http200, data)
## this is also valid will automatic handle JsonNode
## await @!Context.reply(Http200, data)
7.4 Handling XML data
All xml request data will convert to nim stdlib xmltree see https://nim-lang.org/docs/xmltree.html
@!Post "/test-xml":
## lets do xml test
## http://localhost:8000/test-xml with post method
let data = @!Xml ## \
## xml data from client request
## all data will convert to nim stdlib XmlNode
## see https://nim-lang.org/docs/xmltree.html
##
## Try to send data using this xml format
## <Address>
## <City>Ngawi</City>
## <Province>Surabaya</Province>
## </Address>
##
## lets modify the data add country
let country = newElement("Country")
country.add(newText("Indonesia"))
data.add(country)
## katalis will automatic response as xml if we pass XmlNode
## lets pass XmlNode from client and we modify it
await @!Context.replyXml(Http200, data)
## this is also valid will automatic handle XmlNode
## await @!Context.reply(Http200, data)
7.5 Handling uploaded files
@![Get, Post] "/test-upload":
## lets do upload multipart data
## katalis come with mustache template engine
## for template engine we will explain later
##
let tpl =
"""
<html>
<head>
<title>upload test</title>
</head>
<body>
<h3>Upload files</h3>
<form method="POST" enctype="multipart/form-data">
Upload Single
<br>
<input name="onefile" type="file" />
<br>
<br>
Upload Multiple
<br>
<input name="multiplefiles[]" type="file" multiple />
<br>
<br>
<button type="submit">Upload</button>
</form>
</body>
</html>
"""
if @!Req.isHttpPost:
## test show uploaded file info to console
if @!Form.files.len != 0:
for name, files in @!Form.files:
echo name
for file in files:
echo file.path
echo file.mimetype
echo file.isAccessible
echo file.name
## create directory uploaded if not exist
if not "uploaded".dirExists:
"uploaded".createDir
## check if files exists
if "onefile" in @!Form.files:
let onefile = @!Form.files["onefile"][0]
## move files to uploaded dir
onefile.path.moveFile("uploaded".Path/onefile.Path)
if "multiplefiles" in @!Form.files:
let multiplefiles = @!Form.files["multiplefiles"]
for file in multiplefiles:
## move files to uploaded dir
file.path.moveFile("uploaded"/file.name.Path)
let m = newMustache()
@!Context.reply(Http200, m.render(tpl))
7.6 Redirect
We can modify response header for redirection purpose
@!App:
@!Get "/home":
@!Context.reply(Http200, "<h3>Welcome home!</h3>")
@!Get "/test-redirect-custom":
@!Res.headers["Location"] = "/home"
#
# Http302, Http301, Http308, Http303, Http307
# see https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Redirections
#
@!Context.reply(Http307, "")
@!Get "/test-redirect-temporary":
# http code 307
@!Context.replyRedirect("/home")
@!Get "/test-redirect-permanent":
## http code 308
@!Context.replyPermanentRedirect("/home")
7.7 Session
See katalis/core/session.nim
@!App:
## init cookie session
@!Before:
await @!Context.initCookieSession()
@!Get "/hello":
await @!Context.addCookieSession("name", %"Tian Long") ## \
## addCookieSession value must JsonNode
## example
## addCookieSession("profile", %*{"username": "tianlong", "email": "tianlong@mail.com"})
## addCookieSession("brand", %["Toyota", "Ford", "BYD"])
## addCookieSession("counter", %0)
## addCookieSession("temperature", %0.5)
## etc
##
let name = await @!Context.getCookieSession("name")
## remove individual session with @!Context.deleteCookieSession("name")
## destroy all session value with @!Context.destroyCookieSession()
await @!Context.reply(Http200, &"Hello {name}!")
8. Before, After, Middleware, OnReply, Cleanup Pipelines
8.1 Before pipeline
Before pipeline will execute before routing process, also before serving staticfile. We can use it to check for all route before route process. We can skip all route by returning true statement
@!App:
@!Before:
## your code here
if something_wrong:
@!Context.reply(Http403, "Anauthorized access!")
## by returning true, will skip all process and return the error message, this is simplify for checking
return true
8.2 After pipeline
After pipeline will execute after routing process, also after serving staticfile
@!App:
@!After:
## your code here
if something_wrong:
@!Res.headers["Location"] = "/home"
@!Context.reply(Http307, "")
## return true for skip all routing definition
return true
8.3 Midleware
Before and After act like middleware injection, because the block always execute and check before route and after route process.
This is handy hack, for example if we want to validate if user already login or not and we can eliminate add code validation on each route block
proc middlewareHandler(ctx: HttpContext, env: Environment): Future[bool] {.async.} = ## \
##
## return bool:
## return true if something happend and want to skip all process
##
## ctx == @!Context
## env == @!Env
##
## for example we want to check if user already login or not
## if not login then just skip all process with return true
if ($ @!Req.uri).contains("/admin") and not checkIfUserIsLoginAndIsAdmin:
## if request path start with /admin , and user is not admin we need to denied the access
## then redirect to login page
await @!Context.replyRedirect("/login")
## don't forget to return true to make sure rest of route not accessible and break the route pipeline
## for unwanted access
result = true
@!App:
## you can do in @!Before or @!After
@!Before:
##
## this block will always execute and check before route process
## you can check every thing here
##
## you can call here
await @!Context.middlewareHandler(@!Env)
@!After:
##
## this block will always execute and check after route process
## right before execute POST, GET
## you can check every thing here
## or you can also call here
await @!Context.middlewareHandler(@!Env)
8.4 OnReply
OnReply pipeline will process before sending request to client, we can modify for all response from route. This example is from katalis/pipelines/onReply/httpCompress.nim, will compress before zending to client
##
## katalis framework
## This framework if free to use and to modify
## License: MIT
## Author: Amru Rosyada
## Email: amru.rosyada@gmail.com, amru.rosyada@amscloud.co.id
## Git: https://github.com/zendbit/katalis
##
##
## add pipeline on onreply
## check if client support compression then compress
##
import zippy
import
../../core/routes,
../../macros/sugar,
../../core/environment
@!App:
@!OnReply:
# if client support gzip
# and enableCompression enabled
if "gzip" in
@!Req.headers.getValues("accept-encoding") and
@!Settings.enableCompression:
@!Res.headers["content-encoding"] = "gzip"
@!Res.body = compress(@!Res.body, BestSpeed, dfGzip)
8.5 Cleanup
Cleanup pipeline will process after all pipeline finished, this usually for cleanup resource. This example is from katalis/pipelines/cleanup/httpContext.nim
##
## katalis framework
## This framework if free to use and to modify
## License: MIT
## Author: Amru Rosyada
## Email: amru.rosyada@gmail.com, amru.rosyada@amscloud.co.id
## Git: https://github.com/zendbit/katalis
##
##
## Cleanup body request cache
##
import
../../core/routes,
../../macros/sugar,
../../core/environment
@!App:
@!Cleanup:
if @!Req.body.fileExists:
@!Req.body.removeFile
if @!Req.param.form.files.len != 0:
## remove file after file uploaded
## uploaded file should be move after finished
## file uploaded before cleanup present
for _, files in @!Req.param.form.files:
for file in files:
if not file.isAccessible or not file.path.fileExists:
continue
file.path.removeFile
## clear http context
@!Context.clear
9. Response Message
Response message is universal response message, using this response message will always response application/json. See katalis/core/replyMsg
@!App:
@!Get "/test-replymsg":
## see core katalis/core/replyMsg.nim
@!Context.reply(newReplyMsg(
httpCode = Http200,
success = true,
data = %*{
"username": "tian",
"address": "Guangdong"
},
error = %*{}
))
10. Validation
Katalis comes with validations feature. See katalis/plugins/validation.nim.
Available validations are:
- isRequired
- isEmail
- minValue
- maxValue
- minLength
- maxLength
- isDateTime
- minDateTime
- maxDateTime
- inList
- matchWith -> regex validation
- accept(value: string = "") -> accept value and set value, if you want just skip the validation
- check(proc (v: string): bool) -> check value using procedure, must return true if value valid on checking
proc testValidation(ctx: HttpContext) {.gcsafe async.} =
## validation is plugins on katalis
## see katalis/plugins/validation.nim
let tpl =
"""
<html>
<head><title>validation test</title></head>
<body>
<form method="POST">
<label>Username<label> <span>{{fields.username.msg}}</span>
<br>
<input type="text" name="username" value="{{fields.username.value}}">
<br>
<br>
<label>Password</label> <span>{{fields.password.msg}}</span>
<br>
<input type="password" name="password" value="{{fields.password.value}}">
<br>
<br>
<input type="submit" value="Register">
</form>
</body>
</html>
"""
## mustache template
let m = newMustache()
if @!Req.isHttpPost:
## parameter can be Form, JsonNode or Table[string, string] type
let v = newValidation(@!Form)
v.withField("username").
isRequired(failedMsg = "Username is empty."). ## we can add custom failedMsg
minLength(8). ## minimum length of field value is 8 char length
maxLength(50). ## maximum length of field value is 50 char length
matchWith("([a-zA-Z0-9_]+)$", failedMsg = "Only a-z A-Z 0-9 _ are allowed") ## check with regex, only allow a-z A-Z 0-9 _
v.withField("password").
isRequired(failedMsg = "Password is empty."). ## we can add custom failedMsg
minLength(8). ## minimum length 8 char length
maxLength(254) ## maximum length 254 char length
## we can check validation result for each field
## lets print to console
echo "username " & v.fields["username"].msg & " -> " & $v.fields["username"].isValid
echo "password " & v.fields["password"].msg & " -> " & $v.fields["password"].isValid
## set mustache context send data to template
m.data["fields"] = %v.fields
@!Context.reply(Http200, m.render(tpl))
@!App:
@![Get, Post] "/test-validation":
await @!Context.testValidation
Automatic validation initialization using validation pragma
proc testValidation(ctx: HttpContext) {.gcsafe async validation mustacheView.} =
## validation is plugins on katalis
## see katalis/plugins/validation.nim
let tpl =
"""
<html>
<head><title>validation test</title></head>
<body>
<form method="POST">
<label>Username<label> <span>{{fields.username.msg}}</span>
<br>
<input type="text" name="username" value="{{fields.username.value}}">
<br>
<br>
<label>Password</label> <span>{{fields.password.msg}}</span>
<br>
<input type="password" name="password" value="{{fields.password.value}}">
<br>
<br>
<input type="submit" value="Register">
</form>
</body>
</html>
"""
## mustache template using mustacheView pragma
## just call view for mustache instance
if @!Req.isHttpPost:
## parameter can be Form, JsonNode or Table[string, string] type
## with pragma validation automatically append
## so we can just use @!Check for validation instance
@!Check.toCheck = % @!Form.data
@!Check.withField("username").
isRequired(failedMsg = "Username is empty."). ## we can add custom failedMsg
minLength(8). ## minimum length of field value is 8 char length
maxLength(50). ## maximum length of field value is 50 char length
matchWith("([a-zA-Z0-9_]+)$", failedMsg = "Only a-z A-Z 0-9 _ are allowed") ## check with regex, only allow a-z A-Z 0-9 _
@!Check.withField("password").
isRequired(failedMsg = "Password is empty."). ## we can add custom failedMsg
minLength(8). ## minimum length 8 char length
maxLength(254) ## maximum length 254 char length
## we can check validation result for each field
## lets print to console
echo "username " & v.fields["username"].msg & " -> " & $v.fields["username"].isValid
echo "password " & v.fields["password"].msg & " -> " & $v.fields["password"].isValid
## set mustache context send data to template
@!View.data["fields"] = % @!Check.fields
@!Context.reply(Http200, @!View.render(tpl))
@!App:
@![Get, Post] "/test-validation":
await @!Context.testValidation
11. Template engine (Mustache)
Nim come with Mustache template engine. see katalis/plugins/nimMustache.nim, this template based on https://github.com/soasme/nim-mustache.
For using mustache, we need to import mustache from the plugins
import katalis/plugins/nimMustache
For mustache specs, you can refer to https://mustache.github.io/
Mustache can be inline or using .mustache file, in this case we will setup mustache using .mustache.
We need create templates directory
mkdir templates
Then add file index.mustache, header.mustache, footer.mustache. Mustache specs support partials template.
header.mustache
<div>
<h3>This is header section<h3>
</div>
footer.mustache
<div>
<h3>This is footer section<h3>
</div>
Then we will include partials header.mustache and footer.mustache
index.mustache
<html>
<head><title>mustache example</title></head>
<body>
<div>
{{> header}}
<div>
<h3>This is content section<h3>
<h3>{{post.title}}</h3>
<p>{{post.article}}</p>
</div>
{{> footer}}
</div>
</body>
</html>
Mustache using {{tag_mustache}} for data binding, in current nim it support JsonNode, Tables, and mustache Context it self.
Let do with the code
@!App:
@!Get "/test-mustache":
let m = newMustache()
m.data["post"] = %*{"title": "This is katalis", "article": "This is just simple micro framework but powerfull!"}
## call the index.mustache in the templates folder
await @!Context.reply(Http200, m.render("index"))
Automatic initialize mustache using mustacheView pragma
proc testMustache(ctx: HttpContext) {.gcsafe async mustacheView.} =
## with mustacheView pragma on the top of proc will auto append
## so we can just call @!View for the mustache instance
## want to validate http post form data
@!Req.isHttpPost:
@!View.toCheck = % @!Form.data
## pass data to view
@!View.data["post"] = %*{"title": "This is katalis", "article": "This is just simple micro framework but powerfull!"}
## call the index.mustache in the templates folder
await @!Context.reply(Http200, @!View.render("index"))
@!App:
@!Get "/test-mustache":
@!Context.testMustache
12. Web Socket
Out of the box with webscoket. See katalis/core/webSocket.nim
@!App:
## it will accessed with ws://localhost:8000/ws
@!WebSocket "/ws":
if @!WebSocket.isOpen:
if @!WebSocket.isRecvText:
if not @!WebSocket.isRecvContinuation: ## \
## handle msg without continuation flag
echo @!WebSocket.recvMsg ## \
## recieve message from client
await @!WebSocket.replyText("This is from end point.") ## \
## send message to client
## for continuation text use
## await @!WebSocket.replyTextContinuation("data") ## \
## for send binary use
## await @!WebSocket.replyBinary("data")
## for send binary continuation use
## await @!WebSocket.replyBinaryContinuation("data")
else: ## \
## handle msg with continuation flag
echo @!WebSocket.recvMsg
echo "handle with continuation message here"
if @!WebSocket.isRecvBinary: ## \
## recv binary msg
if not @!WebSocket.isRecvContinuation: ## \
## handle msg without continuation flag
echo @!WebSocket.recvMsg
else: ## \
## handle msg with continuation flag
echo @!WebSocket.recvMsg
echo "handle with continuation message here"
if @!WebSocket.isClose:
if @!WebSocket.isError:
echo @!WebSocket.errMsg
echo "Closed"
13. SSE (Server Sent Event)
Using server sent event from katalis just do like this
## test sse
@!Get "/test-sse": ## \
## page template for html sse example
let tpl =
"""
<html>
<head>
<title>example sse</title>
</head>
<body>
<h4>Test sse</h4>
</body>
<ul id="list">
</ul>
<script>
document.addEventListener("DOMContentLoaded", function(e) {
const evtSource = new EventSource(
"/test-sse-event"
)
evtSource.onmessage = (event) => {
const newElement = document.createElement("li")
const eventList = document.getElementById("list")
newElement.textContent = `message: ${event.data}`
eventList.appendChild(newElement)
}
})
</script>
</html>
"""
let v = newMustache()
@!Context.reply(Http200, v.render(tpl))
@!Get "/test-sse-event": ## \
## this is event listen by sse event
await @!Context.replyEventStream(Http200, "message from server..")
## default sse using message as default event name
## if you want to using custom name event pass the event name
##
## await @!Context.replyEventStream(Http200, "message from server..", event: "data")
##
14. Serve SSL
Katalis also support serve SSL, we just need ssl certificate or we can use self signed certificate for development purpose.
Hot to create self signed SSL?, you can follow this instruction https://devcenter.heroku.com/articles/ssl-certificate-self. Or you can find other resources from the internet world.
Then you can pass the certificate to the katalis settings
@!Settings.sslSettings = newSslSettings(
certFile = "domain.crt".Path,
keyFile = "domain.key".Path,
port = Port(8443), ## default value
enableVerify = false ## set to true if using production and valid ssl certificate
)
it will server on https://localhost:8443
15. Deploy to shared hosting that support Common Gateway Interface (CGI)
Katalis support to shared hosting using The Common Gateway Interface (CGI) is a standard protocol that defines how web servers can interact with external programs to process user requests and generate dynamic content.
---Note---
Deploy to shared hosting, mean we use web hosting service, and we cannot run katalis like running on real hardware, vm or docker. Because of the limitation, we cannot use WEBSOCKET, TASKMONITOR. but all others feature is running well. Tested on Litespeed, Apache and Lighttpd
---Note---
To compile for shared hosting with CGI support just pass the -d:cgiapp
nnim c app.nim
after compile the app, you can just upload the executable app with others folder like templates, static in the cgi or cgi-bin folder on then shared hosting
---Note---
Before further using CGI app, you need to know that earch time request we must add query string to the url for routing purpose
for example the app location is in https://mydomain/cgi/app, we need to pass ?uri=/<target_routing>
See this example and this is just straight forward
@!App:
## this mean we need to call using ?uri=/admin
## for full url example https://mydomain/cgi/app?uri=/admin
@!Get "/admin":
CODE_GOES_HERE
## this mean we need to call using ?uri=/user
## for full url example https://mydomain/cgi/app?uri=/user
@!Get "/user":
CODE_GOES_HERE
## this mean we need to call using ?uri=/user/add
## for full url example https://mydomain/cgi/app?uri=/user/add
@!Get "/user/add":
CODE_GOES_HERE
## if your code implementation not supported in CgiApp mode
## then use CgiApp as compile time checking to discard the code on compile time
when not CgiApp:
## thi only work on katlais full framework
@!WebSocket "/chatbots":
CODE_GOES_HERE
@Emit
---Note---
Access static file is also same, for example we put css file in static/css/style.css, we can access it by using https://mydomain/cgi/app?uri=/css/style.css
Just remember, using ?uri=/<target_routing> to mapping with routing. ?uri=/ mean routing to / and will uri not specify then will automatic the app will redirect to ?=uri/
16. Fullstack
Katalis is not fullstack framework, but if you want to use katalis as part of your stack you can use with others framework.
Frontend:
Databse (ORM):
17. Katalis Coding Style Guideline
Katalis coding style guideline is simple
- Follow nim lang Coding Style
- Only use Katalis DSL on the App and Pipeline don't use it on the core, utils to make katalis easy for debugging
18. Katalis structure
Internal katalis structure is devided into some folders structure
18.1 core (folder)
Core folder contains base katalis framework it's focused on http protocol implementataion and some protocols enhancements
| Filename | Description |
|---|---|
| constants.nim | contains constans declaration used by katalis |
| environment.nim | contains shared environment (settings, shared threads variable) |
| form.nim | contains functionalities for construct form (urlencoded, data) |
| httpContext.nim | contains http context per client request (cookie, request, response) |
| katalis.nim | katalis object type and instance |
| multipart.nim | contains functionality for construct multipart data |
| replyMsg.nim | universal response message to client |
| request.nim | request object type used by http context |
| response.nim | response object type used by http context |
| routes.nim | route object type and instance, contains funtionalities for handling route request |
| session.nim | contains funtionalities for handling cookies |
| staticFile.nim | contains funtionalities for handling static file |
| webSocket.nim | websocket object type for handling websocket request |
18.2 Pipelines (folder)
Pipelines in katalis is like middleware, it will process request from client and response with appropriate response. Katalis has some pipelines
| Pipelines | Descriptions |
|---|---|
| after | this will be evaluate after route process |
| before | this will be evaluate before route process |
| initialize | will be eveluate on katalis initialization when katalis start |
| onReply | will be evaluate before response message to client, this usually used for modified response message |
18.2.1 Initialize pipelines
Initialize pipeline will be eveluate on katalis initialization when katalis start.
| Filename | Description |
|---|---|
| taskMonitor.nim | this will start task monitor for katalis |
We can also add custom task with schedules like cron job
18.2.2 Before pipelines
Before pipeline will be evaluate before route processing, this pipeline has advantages like early checking like authentication. Katalis has some predefines before pipelines
| Filename | Description |
|---|---|
| http.nim | handle http request from client (get, post, head, etc) |
| httpRanges.nim | handle http ranges request from client |
| session.nim | session initialization |
| webSocket.nim | handle web socket request from client, if http protocol upgrade request present |
18.2.3 After pipelines
After pipeline will be evaluate after route processing, this pipelines has advantages like early checking if request has access to route resource or not
| Filename | Description |
|---|---|
| httpStaticFile.nim | handle static file request from client |
Static file must be placed is in static folder, but we can also changes default static folder from configuration (For more information about configuration see configuration section).
18.2.4 OnReply pipelines
OnReply pipeline will be evaluate before sending response to client, this pipeline used for modifying payload.
| Filename | Description |
|---|---|
| httpComposePayload.nim | handle composing payload header + body for response |
| httpCompress.nim | handle compression support (gzip) if client support zip compression |
18.2.5 Cleanup pipelines
Clenup pipeline will evaluate after sending response to client, this pipeline will evaluate after all process response to client finished.
| Filename | Description |
|---|---|
| httpContext.nim | will cleanup unused cache data related with http context |
18.3 Macros (folder)
Macros folder contains macros definition for katalis framework
| Filename | Description |
|---|---|
| sugar.nim | macros definition for katalis DSL (Domain Specific Language) |
More information about DSL, see DSL (Domain Specific Languate) section
18.4 Utils (folder)
Utilities and helper for katalis framework
| Filename | Description |
|---|---|
| crypt.nim | some cryptohraphy algorithm |
| debug.nim | debug msg |
| httpcore.nim | http core stdlib [plugins |
| json.nim | some json stdlib plugins |
18.5 Plugins (folder)
Internal plugins for katalis framework
| Filename | Description |
|---|---|
| nimMustache.nim | mustache template engine using mustache nimble pkg |
| taskMonitor.nim | simple cron job for katalis |
| validation.nim | simplify validation for form, json, and Table[string, string] |
18.6 KatalisApp (file)
Katalis application, this is starting poin of katalis framework. Includes all file needed for developing katalis application.
| Filename | Description |
|---|---|
| katalisApp.nim | include this file for starting the app server |
18.7 Pipelines (file)
Katalis pipeline contains include declaration for katalis pipelines order, include declaration is important depend on this order:
- initialize
- before
- after
- onReply
- cleanup
| Filename | Description |
|---|---|
| pipelines.nim | pipeline order includes declaration |
19. Enable trace for debugging and Logging
Trace debug for non web in console default off, we can enabled by call setting
@!Settings.enableTrace = true
for logging and system exception will saved to storages/log/ folder, and file will saved as "dd-MMMM-yyyy" format
if want to add to log file, follow step bellow
import katalis/utils/debug
## add to debug log file
await putLog("your message log")
## or for non async use waitFor
waitFor putLog("your message log")
## also you can use switcher for debugging and production level
## by passing -d:release on compile time
## and we can use IsReleaseMode for selector
import katalis/core/constants
when IsReleaseMode:
AVAILABLE_ON_RELEASE_MODE
else:
AVAILABLE_ON_DEBUG_MODE

_BG.png)



