Xander is an easy to use web application development library and framework for the Nim programming language. Nim is a statically typed language with a Python-like syntax and a powerful macro system, something Xander uses greatly to its advantage.
The easiest way to install Xander is to use Nimble, which is bundled with the Nim installation.
nimble install https://github.com/sunjohanday/Xander.git
Otherwise you can download this git repository and import xander
with the appropriate relative file path, e.g. import ../xander/xander
OPTIONAL If you wish to install Xander CLI, enter the following line on the command line (on Linux):
~/.nimble/pkgs/Xander-0.6.0/Xander/install.sh
You can manually perform the tasks the CLI install.sh
script performs. Simply compile the downloaded xander.nim
file and run the executable.
A basic Xander-app example:
import xander
get "/":
respond "Hello World!"
runForever(3000)
More examples can be found in the examples
folder.
Xander injects variables for the developer to use in request handlers. These variables are:
- request, the http request
- data, contains data sent from the client such as get parameters and form data (shorthad for JsonNode)
- headers, for setting response headers (see request.headers for request headers)
- cookies, for accessing request cookies and setting response cookies
- session, client specific session variables
- files, uploaded files
# Request Handler definition
type
RequestHandler* =
proc(request: Request, data: var Data, headers: var HttpHeaders, cookies: var Cookies, session: var Session, files: var UploadFiles): Response {.gcsafe.}
These variables do a lot of the legwork required in an effective web application.
To serve files from a directory (and its sub-directories)
# app.nim
# the app dir contains a directory called 'public'
serveFiles "/public"
<img src="/public/image.jpg" height="300"/>
<script src="/public/js/main.js"></script>
<link rel="stylesheet" href="/public/css/main.css"/>
Xander provides support for templates, although it is very much a work in progress. To serve a template file:
# Serve the index page
respond tmplt("index")
The default directory for templates is templates
, but it can also be changed by calling
setTemplateDirectory("views")
By having a layout.html
template one can define a base layout for their pages.
<!DOCTYPE html>
<html>
<head>
<title>{[title]}</title>
</head>
<body>
{[ content ]}
{[ template footer ]}
</body>
</html>
In the example above, {[title]}
is a user defined variable, whereas {[ content ]}
is a Xander defined variable, that contains the contents of a template file. To include your own templates, use the template
keyword {[template my-template]}
. You can also include templates that themselves include other templates.
<!-- templates/footer.html -->
<footer>
<a href="/">Home</a>
<!-- templates/contact-details.html -->
{[ template contact-details ]}
</footer>
You can also seperate templates into directories. The nearest layout file will be used: if none is found in the same directory, parent directories will be searched.
appDir/
app.nim
...
templates/
index.html # Root page index
layout.html # Root page layout
register/
index.html # Register page index
# Root page layout
admin/
index.html # Admin page index
layout.html # Admin page layout
normie/
index.html # Client page index
layout.html # Client page layout
For loops are supported in Xander templates. This is still very much a work in progress.
<body>
<table>
<tr>
<th>Name</th>
<th>Age</th>
<th>Hobbies</th>
</tr>
{[ for person in people ]}
<tr>
<td>{[ person.name ]}</td>
<td>{[ person.age ]}</td>
<td>
<ul>
{[ for hobby in person.hobbies ]}
<li>{[ hobby ]}</li>
{[ end ]}
</ul>
</td>
</tr>
{[ end ]}
</table>
</body>
Xander provides a custom type Data
, which is shorthand for JsonNode
, and it also adds some functions to make life easier. To initialize it, one must use the newData()
func. In the initialized variable, one can add key-value pairs
var vars = newData()
vars["name"] = "Alice"
vars["age"] = 21
vars.set("weight", 50)
# or you can initialize it with a key-value pair
var vars = newData("name", "Alice").put("age", 21)
In a template, one must define the variables with matching names. Currently, if no variables are provided, the values will default to empty strings.
<p>{[name]} is {[age]} years old.</p>
To match a custom route and get the provided value(s), one must simply use a colon to specify a dynamic value. The values will be stored in the data
parameter implicitly.
# User requests /countries/ireland/people/paddy
get "/countries/:country/people/:person":
assert(data["country"] == "ireland")
assert(data["person"] == "paddy")
respond tmplt("userPage", data)
)
<h1>{[person]} is from {[country]}</h1>
To add a subdomain to your application simply do the following:
subdomain "api":
# Matches api.mysite.com
get "/":
respond %* {
"status": "OK",
"message": "Hello World!"
}
# Matches api.mysite.com/people
get "/people":
let people = @["adam", "beth", "charles", "david", "emma", "fiona"]
respond newData("people", people)
Xander features the host
macro, which makes it possible to run seperate applications depending on the hostname
of the request header.
# Travel Blog
host "travel-blog.com":
get "/":
respond "Welcome to my travel blog!"
# Tech page
host "cool-techy-site.com":
get "/":
respond "Welcome to my cool techy site!"
subdomain "api":
get "/":
respond %* {
"status": "OK",
"message": "Welcome"
}
Xander uses the ws library provided by https://github.com/treeform/ws.
get "/":
respond tmplt("index")
# echo web socket server
websocket "/ws":
# the websocket variable is injected as 'ws'
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
await ws.send(packet)
As Xander's request handlers only prepare the response to be sent to the client, a way for accessing the onRequest procedure call was added.
Xander exports a variable called requestHook, which the programmer can asign values to. The value should be a anonymous proc as specified below.
# app.nim
requestHook = proc(r: Request) {.async.} =
# Do stuff with the request.
# Nothing actually needs to be done.
# The requestHook procedure is run as soon as
# the request is caught by asynchttpserver.
#
# You could basically make your entire app here.
discard
# app.nim
import xander
# the request hook is essentially the same as
# the 'cb' proc of asynchttpserver.serve, with
# the exception that no responding needs to be done
# (as Xander does it anyways)
requestHook = proc(r: Request) {.async.} =
await r.respond( Http200, "Hello World!" )
runForever(3000)
The requestHook can be used with regular Xander request handlers as per usual.
import xander
get "/":
respond tmplt("index")
requestHook = proc(r: Request) {.async.} =
var ws = await newWebsocket(req)
await ws.sendPacket("Welcome to my echo server!")
while ws.readyState == Open:
let packet = await ws.receiveStrPacket()
await ws.send(packet)
runForever(3000)
- Expanding templates with if-statements
- Windows optimization
- HTTPS, this requires a look into asyncnet and/or net
- Redoing the server architecture without asynchttpserver