Lux is a framework for creating JSON:API backends in Node.

To try it out, let’s create a web service for rating dishes at restaurants. We’ll call it “Opinion Ate”.

Looking for something slightly different? Check the official JSON:API implementations page for alternative Node.js server libraries.

First, install Lux globally:

$ npm install -g lux-framework

Create a new Lux app:

$ lux new opinion-ate
$ cd opinion-ate

This will create an app configured to store data in a SQLite database, which is just a flat file. This is the simplest way to go for experimentation purposes.

Our First Resource

JSON:API represents the data in your apps as “resources”, and Lux provides a single command to set up everything you need for a given resource. First let’s create a resource representing a restaurant. Run the following command in the terminal:

$ lux generate resource restaurant name:string address:string \
  dishes:has-many

This tells Lux to create a new resource called restaurant and to define three fields on it. There are two string fields, name and address. There’s also a has-many relationship, which means that a restaurant can have many dishes associated with it.

The generator created a number of files; let’s take a look at each of them.

First, open the file in db/migrate that ends with -create-restaurants.js — the date on the file will be different than mine, showing the time you ran the command.

export function up(schema) {
  return schema.createTable('restaurants', table => {
    table.increments('id');
    table.string('name');
    table.string('address');
    table.timestamps();

    table.index('created_at');
    table.index('updated_at');
  });
}

export function down(schema) {
  return schema.dropTable('restaurants');
}

This file contains a migration, a class that tells Lux how to make a change to a database. This file will createTable('restaurants'), which is just what it sounds like. The arrow function passed to createTable() receives a parameter table, representing a table. table.increments() creates an auto-incrementing primary key column. table.string() creates a new string column, and table.timestamps() creates created_at and updated_at columns that Lux will manage for us automatically. Finally, table.index() creates database indexes allowing for performant sorting of the created_at and updated_at columns.

The restaurants table hasn’t actually been created yet; the migration file just records how to create it. You can run it on your computer, when a coworker pulls it down she can run it on hers, and you can run it on the production server as well. Run the migration now with this command:

$ lux db:migrate

Next let’s look at the app/models/restaurant.js file created:

import { Model } from 'lux-framework';

class Restaurant extends Model {
  static hasMany = {
    dishes: {
      inverse: 'restaurant'
    }
  };
}

export default Restaurant;

We have a Restaurant class that inherits from Model. The hasMany relationship to dishes is configured on it, but there’s nothing else in the class. How does Lux know what columns are available? It will automatically inspect the table to see what columns are defined on it and make those columns available; no configuration is needed.

Next, take a look at app/serializers/restaurant.js:

import { Serializer } from 'lux-framework';

class RestaurantsSerializer extends Serializer {
  attributes = [
    'name',
    'address'
  ];

  hasMany = [
    'dishes'
  ];
}

export default RestaurantsSerializer;

Serializers translate models to their end-user-facing format. In this case, name and address are configured as columns that should be made available to end users. If we wanted some columns to only be used on the backend, we could remove them from the serializer. It also specifies that the hasMany relationship to dishes should be exposed,

Finally, take a look at app/controllers/restaurants.js:

import { Controller } from 'lux-framework';

class RestaurantsController extends Controller {
  params = [
    'name',
    'address',
    'dishes'
  ];
}

export default RestaurantsController;

For security purposes, controllers don’t allow just any arbitrary parameters to be passed in; they need to be explicitly listed in the params property. If we wanted some columns to be read-only we could remove them from here.

The last piece of the puzzle that was set up for us is the route. Check out app/routes.js:

export default function routes() {
  this.resource('restaurants');
}

This will set up all necessary routes for restaurants:

  • GET /restaurants — lists all the restaurants
  • POST /restaurants — creates a new restaurant
  • GET /restaurants/:id — gets one restaurant
  • PATCH /restaurants/:id — updates a restaurant
  • DELETE /restaurants/:id — deletes a restaurant

Next, let’s create a dish resource:

$ lux generate resource dish name:string rating:integer \
  restaurant:belongs-to

You’ve seen a string column before, and you can probably guess what integer does. belongs-to is the inverse of the has-many relationship–it creates a foreign key column that references another model, in this case Restaurant.

This command generates a model, serializer, and controller for Dishes, and added the appropriate route. You can take a look at the files if you like; they’re similar to the Restaurant files we looked at earlier.

Go ahead and migrate the database again:

$ lux db:migrate

With that, we’re done building out our app, and we didn’t have to write a single line of code–the generator handled it all for us!

Trying It Out

Now let’s give our app a try. Start the Lux server:

$ lux serve

Since we don’t have any data in our server yet, let’s create a restaurant. We won’t be able to do this in the browser; we’ll need a more sophisticated web service client to do so. One good option is Postman—download it and start it up.

Create a POST request to http://localhost:4000/restaurants. Go to the Headers tab and enter key “Content-Type” and value “application/vnd.api+json”—this is the content type JSON:API requires.

Next, switch to the Body tab. Click the “none” dropdown and change it to “raw”. Another “Text” dropdown will appear; change it to “JSON”. Enter the following:

{
  "data": {
    "type": "restaurants",
    "attributes": {
      "name": "Sushi Place",
      "address": "123 Main Street"
    }
  }
}

Now that our request is set up, click Send and you should get a “201 Created” response, with the following body:

{
  "data": {
    "id": "1",
    "type": "restaurants",
    "attributes": {
      "name": "Sushi Place",
      "address": "123 Main Street"
    },
    "relationships": {
      "dishes": {
        "data": []
      }
    }
  },
  "links": {
    "self": "http://localhost:4000/restaurants"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

This is a JSON:API response for a single record. Let’s talk about what’s going on here:

  • The top-level data property contains the main data for the response. In this case it’s one record; it can also be an array.
  • The record contains an id property giving the record’s publicly-exposed ID, which by default is the database integer ID. But JSON:API IDs are always exposed as strings, to allow for the possibility of slugs or UUIDs.
  • Even if you can infer the type of the record from context, JSON:API records always have a type field recording which type they are. In some contexts, records of different types will be intermixed in an array, so this keeps them distinct.
  • attributes is an object containing all the attributes we exposed. They are nested instead of directly on the record to avoid colliding with other standard JSON:API properties like type.
  • relationships provides data on the relationships for this record. In this case, the record has a dishes relationship, but it doesn’t have any related records in it yet.

Now that we have a restaurant, let’s retrieve the data for it. In a new tab, send a GET request to http://localhost:4000/restaurants. You should get the following response:

{
  "data": [
    {
      "id": "1",
      "type": "restaurants",
      "attributes": {
        "name": "Sushi Place",
        "address": "123 Main Street"
      },
      "relationships": {
        "dishes": {
          "data": []
        }
      },
      "links": {
        "self": "http://localhost:4000/restaurants/1"
      }
    }
  ],
  "links": {
    "self": "http://localhost:4000/restaurants",
    "first": "http://localhost:4000/restaurants",
    "last": "http://localhost:4000/restaurants",
    "prev": null,
    "next": null
  },
  "jsonapi": {
    "version": "1.0"
  }
}

Note that this time data is an array. For now it only contains one record.

Let’s see how we can create related data as well. To add a new dish associated with restaurant 1, POST to http://localhost:4000/dishes:

{
  "data": {
    "type": "dishes",
    "attributes": {
      "name": "Volcano Roll",
      "rating": 4
    },
    "relationships": {
      "restaurant": {
        "data": {
          "type": "restaurants",
          "id": "1"
        }
      }
    }
  }
}

You should get the following response:

{
  "data": {
    "id": "1",
    "type": "dishes",
    "attributes": {
      "name": "Volcano Roll",
      "rating": 4
    },
    "relationships": {
      "restaurant": {
        "data": {
          "id": "1",
          "type": "restaurants"
        },
        "links": {
          "self": "http://localhost:4000/restaurants/1"
        }
      }
    }
  },
  "links": {
    "self": "http://localhost:4000/dishes"
  },
  "jsonapi": {
    "version": "1.0"
  }
}

If you’d like to try out updating and deleting records:

  • Make a PATCH request to http://localhost:4000/restaurants/1, passing in updated attributes.
  • Make a DELETE request to http://localhost:4000/restaurants/1 with no body to delete the record.

There’s More

We’ve seen a ton of help Lux has provided us: the ability to create, read, update, and delete records, including record relationships. But it offers a lot more too! It allows you to configure validation errors, allows you to request only a subset of the fields you need, allows you to include related records in the response, as well as sorting, filtering, and pagination. To learn more, check out the Lux Guide.

Now that you have a JSON:API backend, you should try connecting to it from the frontend. Choose a tutorial from the How to JSON:API home page!