Getting Started

This practical introduction will take you from an empty folder to a firstFoxx service installed in ArangoDB, with custom endpoints which can handleuser input and query the database.

A video guide is also available:

Manifest

We’re going to start with an empty folder. This will be the root folder of ourservices. You can name it something clever but for the course of this guidewe’ll assume it’s called the name of your service: getting-started.

First we need to create a manifest. Create a new file called manifest.jsonand add the following content:

  1. {
  2. "engines": {
  3. "arangodb": "^3.0.0"
  4. }
  5. }

This just tells ArangoDB the service is compatible with versions 3.0.0 andlater (all the way up to but not including 4.0.0), allowing older versionsof ArangoDB to understand that this service likely won’t work for them andnewer versions what behavior to emulate should they still support it.

The little hat to the left of the version number is not a typo, it’s called a“caret” and indicates the version range. Foxx uses semantic versioning (alsocalled “semver”) for most of its version handling. You can find out more abouthow semver works at the official semver website.

Next we’ll need to specify an entry point to our service. This is theJavaScript file that will be executed to define our service’s HTTP endpoints.We can do this by adding a “main” field to our manifest:

  1. {
  2. "engines": {
  3. "arangodb": "^3.0.0"
  4. },
  5. "main": "index.js"
  6. }

That’s all we need in our manifest for now.

Router

Let’s next create the index.js file:

  1. 'use strict';
  2. const createRouter = require('@arangodb/foxx/router');
  3. const router = createRouter();
  4. module.context.use(router);

The first line causes our file to be interpreted usingstrict mode.All examples in the ArangoDB documentation assume strict mode, so you mightwant to familiarize yourself with it if you haven’t encountered it before.

The second line imports the @arangodb/foxx/router module which provides afunction for creating new Foxx routers. We’re using this function to create anew router object which we’ll be using for our service.

The module.context is the so-called Foxx context or service context.This variable is available in all files that are part of your Foxx service andprovides access to Foxx APIs specific to the current service, like the usemethod, which tells Foxx to mount the router in this service (and to exposeits routes to HTTP).

Next let’s define a route that prints a generic greeting:

  1. // continued
  2. router.get('/hello-world', function (req, res) {
  3. res.send('Hello World!');
  4. })
  5. .response(['text/plain'], 'A generic greeting.')
  6. .summary('Generic greeting')
  7. .description('Prints a generic greeting.');

The router provides the methods get, post, etc corresponding to eachHTTP verb as well as the catch-all all. These methods indicate that the givenroute should be used to handle incoming requests with the given HTTP verb(or any method when using all).

These methods take an optional path (if omitted, it defaults to "/") as wellas a request handler, which is a function taking the req(request) and res(response) objects to handle the incomingrequest and generate the outgoing response. If you have used the expressframework in Node.js, you may already be familiar with how this works,otherwise check out the chapter on routes.

The object returned by the router’s methods provides additional methods toattach metadata and validation to the route. We’re using summary anddescription to document what the route does — these aren’t strictlynecessary but give us some nice auto-generated documentation.The response method lets us additionally document the response contenttype and what the response body will represent.

Try it out

At this point you can upload the service folder as a zip archive from theweb interface using the Services tab.

Click Add Service then pick the Zip option in the dialog. You will needto provide a mount path, which is the URL prefix at which the service willbe mounted (e.g. /getting-started).

Once you have picked the zip archive using the file picker, the upload shouldbegin immediately and your service should be installed. Otherwise press theInstall button and wait for the dialog to disappear and the service to showup in the service list.

Click anywhere on the card with your mount path on the label to open theservice’s details.

In the API documentation you should see the route we defined earlier(/hello-world) with the word GET next to it indicating the HTTP method itsupports and the summary we provided on the right. By clicking on theroute’s path you can open the documentation for the route.

Note that the description we provided appears in the generated documentationas well as the description we added to the response (which should correctlyindicate the content type text/plain, i.e. plain text).

Click the Try it out! button to send a request to the route and you shouldsee an example request with the service’s response: “Hello World!”.

Congratulations! You have just created, installed and used your first Foxx service.

Parameter validation

Let’s add another route that provides a more personalized greeting:

  1. // continued
  2. const joi = require('joi');
  3. router.get('/hello/:name', function (req, res) {
  4. res.send(`Hello ${req.pathParams.name}`);
  5. })
  6. .pathParam('name', joi.string().required(), 'Name to greet.')
  7. .response(['text/plain'], 'A personalized greeting.')
  8. .summary('Personalized greeting')
  9. .description('Prints a personalized greeting.');

The first line imports the joi module from npmwhich comes bundled with ArangoDB. Joi is a validation library that is usedthroughout Foxx to define schemas and parameter types.

Note: You can bundle your own modules from npm by installing them in yourservice folder and making sure the node_modules folder is included in yourzip archive. For more information see the chapter onbundling node modules.

The pathParam method allows us to specify parameters we are expecting inthe path. The first argument corresponds to the parameter name in the path,the second argument is a joi schema the parameter is expected to match andthe final argument serves to describe the parameter in the API documentation.

The path parameters are accessible from the pathParams property of therequest object. We’re using a template string to generate the server’sresponse containing the parameter’s value.

Note that routes with path parameters that fail to validate for the request URLwill be skipped as if they wouldn’t exist. This allows you to define multipleroutes that are only distinguished by the schemas of their path parameters (e.g.a route taking only numeric parameters and one taking any string as a fallback).

Let’s take this further and create a route that takes a JSON request body:

  1. // continued
  2. router.post('/sum', function (req, res) {
  3. const values = req.body.values;
  4. res.send({
  5. result: values.reduce(function (a, b) {
  6. return a + b;
  7. }, 0)
  8. });
  9. })
  10. .body(joi.object({
  11. values: joi.array().items(joi.number().required()).required()
  12. }).required(), 'Values to add together.')
  13. .response(joi.object({
  14. result: joi.number().required()
  15. }).required(), 'Sum of the input values.')
  16. .summary('Add up numbers')
  17. .description('Calculates the sum of an array of number values.');

Note that we used post to define this route instead of get (which does notsupport request bodies). Trying to send a GET request to this route’s URL(in the absence of a get route for the same path) will result in Foxxresponding with an appropriate error response, indicating the supportedHTTP methods.

As this route not only expects a JSON object as input but also responds witha JSON object as output we need to define two schemas. We don’t strictly needa response schema but it helps documenting what the route should be expectedto respond with and will show up in the API documentation.

Because we’re passing a schema to the response method we don’t need toexplicitly tell Foxx we are sending a JSON response. The presence of a schemain the absence of a content type always implies we want JSON. Though we couldjust add ["application/json"] as an additional argument after the schema ifwe wanted to make this more explicit.

The body method works the same way as the response method except the schemawill be used to validate the request body. If the request body can’t be parsedas JSON or doesn’t match the schema, Foxx will reject the request with anappropriate error response.

Creating collections

The real power of Foxx comes from interacting with the database itself.In order to be able to use a collection from within our service, we shouldfirst make sure that the collection actually exists. The right place to createcollections your service is going to use is ina setup script, which Foxx will execute for you wheninstalling or updating the service.

First create a new folder called “scripts” in the service folder, which willbe where our scripts are going to live. For simplicity’s sake, our setupscript will live in a file called setup.js inside that folder:

  1. // continued
  2. 'use strict';
  3. const db = require('@arangodb').db;
  4. const collectionName = 'myFoxxCollection';
  5. if (!db._collection(collectionName)) {
  6. db._createDocumentCollection(collectionName);
  7. }

The script uses the db object fromthe @arangodb module, which lets us interact with the database the Foxxservice was installed in and the collections inside that database. Because thescript may be executed multiple times (i.e. whenever we update the service orwhen the server is restarted) we need to make sure we don’t accidentally tryto create the same collection twice (which would result in an exception);we do that by first checking whether it already exists before creating it.

The _collection method looks up a collection by name and returns null if nocollection with that name was found. The _createDocumentCollection methodcreates a new document collection by name (_createEdgeCollection also existsand works analogously for edge collections).

Note: Because we have hardcoded the collection name, multiple copies ofthe service installed alongside each other in the same database will sharethe same collection.Because this may not always be what you want, the Foxx contextalso provides the collectionName method which applies a mount point specificprefix to any given collection name to make it unique to the service. It alsoprovides the collection method, which behaves almost exactly like db._collectionexcept it also applies the prefix before looking the collection up.

Next we need to tell our service about the script by adding it to the manifest file:

  1. {
  2. "engines": {
  3. "arangodb": "^3.0.0"
  4. },
  5. "main": "index.js",
  6. "scripts": {
  7. "setup": "scripts/setup.js"
  8. }
  9. }

The only thing that has changed is that we added a “scripts” field specifyingthe path of the setup script we just wrote.

Go back to the web interface and update the service with our new code, thencheck the Collections tab. If everything worked right, you should see a newcollection called “myFoxxCollection”.

Accessing collections

Let’s expand our service by adding a few more routes to our index.js:

  1. // continued
  2. const db = require('@arangodb').db;
  3. const errors = require('@arangodb').errors;
  4. const foxxColl = db._collection('myFoxxCollection');
  5. const DOC_NOT_FOUND = errors.ERROR_ARANGO_DOCUMENT_NOT_FOUND.code;
  6. router.post('/entries', function (req, res) {
  7. const data = req.body;
  8. const meta = foxxColl.save(req.body);
  9. res.send(Object.assign(data, meta));
  10. })
  11. .body(joi.object().required(), 'Entry to store in the collection.')
  12. .response(joi.object().required(), 'Entry stored in the collection.')
  13. .summary('Store an entry')
  14. .description('Stores an entry in the "myFoxxCollection" collection.');
  15. router.get('/entries/:key', function (req, res) {
  16. try {
  17. const data = foxxColl.document(req.pathParams.key);
  18. res.send(data)
  19. } catch (e) {
  20. if (!e.isArangoError || e.errorNum !== DOC_NOT_FOUND) {
  21. throw e;
  22. }
  23. res.throw(404, 'The entry does not exist', e);
  24. }
  25. })
  26. .pathParam('key', joi.string().required(), 'Key of the entry.')
  27. .response(joi.object().required(), 'Entry stored in the collection.')
  28. .summary('Retrieve an entry')
  29. .description('Retrieves an entry from the "myFoxxCollection" collection by key.');

We’re using the save and document methods of the collection object to storeand retrieve documents in the collection we created in our setup script.Because we don’t care what the documents look like we allow any attributes onthe request body and just accept an object.

Because the key will be automatically generated by ArangoDB when one wasn’tspecified in the request body, we’re using Object.assign to apply theattributes of the metadata object returned by the save method to the documentbefore returning it from our first route.

The document method returns a document in a collection by its _key or _id.However when no matching document exists it throws an ArangoError exception.Because we want to provide a more descriptive error message than ArangoDB doesout of the box, we need to handle that error explicitly.

All ArangoError exceptions have a truthy attribute isArangoError that helpsyou recognizing these errors without having to worry about instanceof checks.They also provide an errorNum and an errorMessage. If you want to check forspecific errors you can just import the errors object from the @arangodbmodule instead of having to memorize numeric error codes.

Instead of defining our own response logic for the error case we just useres.throw, which makes the response object throw an exception Foxx canrecognize and convert to the appropriate server response. We also pass alongthe exception itself so Foxx can provide more diagnostic information when wewant it to.

We could extend the post route to support arrays of objects as well, eachobject following a certain schema:

  1. // store schema in variable to make it re-usable, see .body()
  2. const docSchema = joi.object().required().keys({
  3. name: joi.string().required(),
  4. age: joi.number().required()
  5. }).unknown(); // allow additional attributes
  6. router.post('/entries', function (req, res) {
  7. const multiple = Array.isArray(req.body);
  8. const body = multiple ? req.body : [req.body];
  9. let data = [];
  10. for (var doc of body) {
  11. const meta = foxxColl.save(doc);
  12. data.push(Object.assign(doc, meta));
  13. }
  14. res.send(multiple ? data : data[0]);
  15. })
  16. .body(joi.alternatives().try(
  17. docSchema,
  18. joi.array().items(docSchema)
  19. ), 'Entry or entries to store in the collection.')
  20. .response(joi.alternatives().try(
  21. joi.object().required(),
  22. joi.array().items(joi.object().required())
  23. ), 'Entry or entries stored in the collection.')
  24. .summary('Store entry or entries')
  25. .description('Store a single entry or multiple entries in the "myFoxxCollection" collection.');

Writing database queries

Storing and retrieving entries is fine, but right now we have to memorize eachkey when we create an entry. Let’s add a route that gives us a list of thekeys of all entries so we can use those to look an entry up in detail.

The naïve approach would be to use the toArray() method to convert the entirecollection to an array and just return that. But we’re only interested in thekeys and there might potentially be so many entries that first retrieving everysingle document might get unwieldy. Let’s write a short AQL query to do this instead:

  1. // continued
  2. const aql = require('@arangodb').aql;
  3. router.get('/entries', function (req, res) {
  4. const keys = db._query(aql`
  5. FOR entry IN ${foxxColl}
  6. RETURN entry._key
  7. `);
  8. res.send(keys);
  9. })
  10. .response(joi.array().items(
  11. joi.string().required()
  12. ).required(), 'List of entry keys.')
  13. .summary('List entry keys')
  14. .description('Assembles a list of keys of entries in the collection.');

Here we’re using two new things:

The _query method executes an AQL query in the active database.

The aql template string handler allows us to write multi-line AQL queries andalso handles query parameters and collection names. Instead of hardcoding thename of the collection we want to use in the query we can simply reference thefoxxColl variable we defined earlier – it recognizes the value as an ArangoDBcollection object and knows we are specifying a collection rather than a regularvalue even though AQL distinguishes between the two.

Note: If you aren’t used to JavaScript template strings and template stringhandlers just think of aql as a function that receives the multiline stringsplit at every ${} expression as well as an array of the values of thoseexpressions – that’s actually all there is to it.

Alternatively, here’s a version without template strings (notice how muchcleaner the aql version will be in comparison when you have multiple variables):

  1. const keys = db._query(
  2. 'FOR entry IN @@coll RETURN entry._key',
  3. {'@coll': foxxColl.name()}
  4. );

Next steps

You now know how to create a Foxx service from scratch, how to handle userinput and how to access the database from within your Foxx service to store,retrieve and query data you store inside ArangoDB. This should allow you tobuild meaningful APIs for your own applications but there are many more thingsyou can do with Foxx. See the Guides chapter for more.