11 KiB
Foxx
Foxx: Build APIs and simple web applications in ArangoDB
Foxx is an easy way to create APIs and simple web applications from within ArangoDB. It is inspired by Sinatra, the classy Ruby web framework. If FoxxApplication is Sinatra, ArangoDB Actions are the corresponding Rack
. They provide all the HTTP goodness.
So let's get started, shall we?
An application build with Foxx is written in JavaScript and deployed to ArangoDB directly. ArangoDB serves this application, you do not need a separate application server.
So given you want to build an application that sends a plain-text response "Worked!" for all requests to /my/wiese
. How would you achieve that with Foxx?
First, create a directory my_app
and save a file called app.js
in this directory. Write the following content to this file:
FoxxApplication = require("org/arangodb/foxx").FoxxApplication;
app = new FoxxApplication();
app.get("/wiese", function(req, res) {
res.set("Content-Type", "text/plain");
res.body = "Worked!"
});
app.start(applicationContext);
This is your application. Now we need to mount it to the path /my
. In order to achieve that, we create a file called manifest.json
in our my_app
directory with the following content:
{
"apps": {
"/my": "app.js"
}
}
Now your application is done. Start ArangoDB as follows:
arangod --javascript.dev-app-path my_app /tmp/fancy_db
Now point your browser to /my/wiese
and you should see "Worked!". After this short overview, let's get into the details.
Details on FoxxApplication
new FoxxApplication
@copydetails JSF_foxx_application_initializer
FoxxApplication#start
@copydetails JSF_foxx_application_start
requiresLibs
and requiresModels
Using the base paths defined in the manifest file, you can require libraries (and the special models described later) that you need in this FoxxApplication. So for example:
app.requiresLibs = {
"schafspelz": "wolf"
};
This will require the file wolf.js
in the libs folder you have defined and make the module available via the variable schafspelz
in your FoxxApplication definitions:
app.get("/bark", function (req, res) {
schafspelz.bark();
});
Please note that you cannot use the normal require syntax in a FoxxApplication
, because it's a special DSL and not a normal JavaScript file.
Handling Requests
If you do not redefine it, all requests that go to the root of your application will be redirected to index.html
.
FoxxApplication#head
@copydetails JSF_foxx_application_head
FoxxApplication#get
@copydetails JSF_foxx_application_get
FoxxApplication#post
@copydetails JSF_foxx_application_post
FoxxApplication#put
@copydetails JSF_foxx_application_put
FoxxApplication#patch
@copydetails JSF_foxx_application_patch
FoxxApplication#delete
@copydetails JSF_foxx_application_delete
Documenting and Constraining the Routes
If you define a route like described above, you have the option to match parts of the URL to variables. If you for example define a route /animals/:animal
and the URL animals/foxx
is requested it is matched by this rule. You can then get the value of this variable (in this case "foxx") by calling request.params("animal")
.
Furthermore you can describe your API by chaining the following methods onto your path definition. With the provided information, Foxx will generate a nice documentation for you. Some of the methods additionally will check certain properties of the request.
Describing a pathParam
/// @fn JSF_foxx_RequestContext_pathParam
Describing a queryParam
/// @fn JSF_foxx_RequestContext_queryParam
Before and After Hooks
You can use the following two functions to do something before or respectively after the normal routing process is happening. You could use that for logging or to manipulate the request or response (translate it to a certain format for example).
FoxxApplication#before
@copydetails JSF_foxx_application_before
FoxxApplication#after
@copydetails JSF_foxx_application_after
More functionality
FoxxApplication#helper
@copydetails JSF_foxx_application_helper
FoxxApplication#accepts
@copydetails JSF_foxx_application_accepts
Models
If you do not require a module with requiresLibs
, but instead use requiresModels
, you will have three additional functions available in the module (otherwise it will behave the same):
var a = requireModel("module")
: Requires the model "module" (which again will have these functions available) and makes it available via the variablea
.appCollection("test")
: Returns the collection "test" prefixed with the application prefixappCollectionName("test")
: Returns the name of the collection "test" prefixed with the application prefix
If you need access to a normal module from this model, just require it as usual.
The Request and Response Objects
When you have created your FoxxApplication you can now define routes on it. You provide each with a function that will handle the request. It gets two arguments (four, to be honest. But the other two are not relevant for now):
- The
request
object - The
response
object
These objects are provided by the underlying ArangoDB actions and enhanced by the BaseMiddleware provided by Foxx.
The request
object
Every request object has the following attributes from the underlying Actions, amongst others:
request.path
This is the complete path as supplied by the user as a String.
request.body
@copydetails JSF_foxx_BaseMiddleware_request_body
request.params
@copydetails JSF_foxx_BaseMiddleware_request_params
The response
object
Every response object has the following attributes from the underlying Actions:
request.body
You provide your response body as a String here.
request.status
@copydetails JSF_foxx_BaseMiddleware_response_status
request.set
@copydetails JSF_foxx_BaseMiddleware_response_set
request.json
@copydetails JSF_foxx_BaseMiddleware_response_json
request.render
@copydetails JSF_foxx_BaseMiddleware_response_render
The Manifest File
In the manifest.json
you define the components of your application: The content is a JSON object with the following keys:
name
: Name of the application (Meta information)version
: Current version of the application (Meta information)description
: A short description of the application (Meta information)thumbnail
: Path to a thumbnail that represents the application (Meta information)apps
: Map routes to FoxxApplicationslib
: Base path for the models you want to requiremodels
: Base path for the models you want to requirefiles
: Deliver filesassets
: Deliver pre-processed filessetup
: Path to a setup scriptteardown
: Path to a teardown script
A more complete example for a Manifest file:
{
"name": "my_website",
"version": "1.2.1",
"description": "My Website with a blog and a shop",
"thumnail": "images/website-logo.png",
"apps": {
"/blog": "apps/blog.js",
"/shop": "apps/shop.js"
},
"lib": "lib",
"models": "models",
"files": {
"/images": "images"
},
"assets": {
"application.js": [
"vendor/jquery.js",
"assets/javascripts/*"
]
},
"setup": "scripts/setup.js",
"teardown": "scripts/teardown.js"
}
setup
and teardown
You can provide a path to a JavaScript file that prepares ArangoDB for your application (or respectively removes it entirely). These scripts have access to appCollection
and appCollectionName
just like models. Use the setup
script to create all collections your application needs and fill them with initial data if you want to. Use the teardown
script to remove all collections you have created.
apps
is an object that matches routes to files:
- The
key
is the route you want to mount at - The
value
is the path to the JavaScript file containing theFoxxApplication
you want to mount
You can add multiple applications in one manifest this way.
The files
Deliver all files in a certain folder without modifying them. You can deliver text files as well as binaries:
"files": {
"/images": "images"
}
The assets
The value for the asset key is an object consisting of paths that are matched to the files they are composed of. Let's take the following example:
"assets": { "application.js": [ "vendor/jquery.js", "assets/javascripts/*" ] }
If a request is made to /application.js
(in development mode), the array provided will be processed one element at a time. The elements are paths to files (with the option to use wildcards). The files will be concatenated and delivered as a single file.
Development Mode
If you start ArangoDB with the option --javascript.dev-app-path
followed by the path to a directory containing a manifest file, you are starting ArangoDB in development mode with the application loaded. This means that on every request:
- All routes are dropped
- All module caches are flushed
- Your manifest file is read
- All files in your lib folder are loaded
- All
apps
are executed. Eachapp.start()
will put the routes in a temporary route object, prefixed with the path given in the manifest - All routes in the temporary route object are stored to the routes
- The request will be processed
This means that you do not have to restart ArangoDB if you change anything in your app. It is of course not meant for production, because the reloading makes the app relatively slow.
Deploying on Production
The Production mode is in development right now.
We will offer the option to process all assets at once and write the files to disk for production with the option to run Uglify2.js
and similar tools in order to compress them.
Optional Functionality: FormatMiddleware
Unlike the BaseMiddleware
this Middleware is only loaded if you want it. This Middleware gives you Rails-like format handling via the extension
of the URL or the accept header. Say you request an URL like /people.json
:
The FormatMiddleware
will set the format of the request to JSON and then delete the .json
from the request. You can therefore write handlers that do not take an extension
into consideration and instead handle the format via a simple String. To determine the format of the request it checks the URL and then the accept
header. If one of them gives a format or both give the same, the format is set. If the formats are not the same, an error is raised.
Use it by calling:
FormatMiddleware = require('foxx').FormatMiddleware;
app.before("/*", FormatMiddleware.new(['json']));
or the shortcut:
app.accepts(['json']);
In both forms you can give a default format as a second parameter, if no format could be determined. If you give no defaultFormat
this case will be handled as an error.