Fork me on GitHub

A quick tour of the Silicon web framework: A simple blog API in 85 C++ lines

In late January 2015, I released the first version of the Silicon Web Framework. The documentation covers all the concepts of the library but does not contains a concrete example covering the needs of a real world application. In this blog post, I'll show how to write a such an application with the framework. Like most modern web apps, it relies on a database to store data, and sessions to authenticate its users.

The source code of this article is hosted on the Silicon github repository.

The user and post models

Let's first define the models of the post and user object that will be persisted in the database. The post object stores the blog posts, while the user object stores logins and passwords.

typedef decltype(D(_id(_auto_increment, _primary_key) = int(),
                   _login                             = std::string(),
                   _password                          = std::string()))
  user;

typedef decltype(D(_id(_auto_increment, _primary_key) = int(),
                   _user_id(_read_only)               = int(),
                   _title                             = std::string(),
                   _body                              = std::string()))
  post;

These two objects are like plain C objects. The only difference is that their type encode compile time information required by the framework: The pairs of symbols/types representing the object members and some extra tags:

Object relational mapping

From these models, we can easily generate the orm and their factories. The sql_orm class provides methods to easily update, insert and delete object from the sqlite database:

typedef sql_orm_factory<sqlite_connection, user> user_orm_factory;
typedef sql_orm<sqlite_connection, user> user_orm;

typedef sql_orm_factory<sqlite_connection, post> post_orm_factory;
typedef sql_orm<sqlite_connection, post> post_orm;

Session and authentication

The next step is to create the session middleware class handling our user authentication and tracking. I rely on hashmap_session_factory to stores the sessions in a in-memory key-value store. If your application needs the sessions to persist after a server reboot, you can use mysql_session or sqlite_session.

The user_id member of the middleware stores the id of the authenticated user, and the methods authenticate, logout and is_authenticated handle user authentication.

struct session
{
  session() : user_id(-1) {}

  bool authenticate(sqlite_connection& c, std::string login, std::string password)
  {
    auto res = c("SELECT user_id FROM blog_users where login = ? and password = ?")
                (login, hash_sha3_512(password));

    auto user_exists = !res.empty();
    if (user_exists)
      res >> user_id;
    return user_exists;
  }
  bool logout() { user_id = -1; }
  bool is_authenticated() { return user_id != -1; }
  int user_id;
};

Several procedures will require the user to be authenticated. They need to sends back a error 401 to the client if it is not authenticated. The restricted_area middleware fulfills this task:

struct restricted_area
{
  static instantiate(session& u)
  {
    if (!u.is_authenticated())
      throw error::unauthorized("Only authenticated users can execute this request.");
  }
};

Why do we need a middleware to check for authentication? Middlewares in Silicon are instantiated before each procedure call, and if their instantiation throws, the exception will be caught by the backend and send back to the client as a HTTP error. This is the behavior we need for our restricted_area middleware. A procedure requires the instantiation of a middleware by simply declaring it as argument:

[] (restricted_area) {
  // A procedure for logged users only.
}

Note that restricted_area::instantiate depends on a session object. The framework automatically instantiate it before each call to restricted_area::instantiate. Actually, Silicon handles any dependency graph between the middleware, as long as their is no cycle. Read more about the dependency injection module.

The API

The API is now a matter of few lines of code. It contains two procedures for login/logout and a set create update destroy (CRUD) procedures for the user and post objects.

Let's start with the login procedure:

auto blog_api = http_api(

  POST / _login * post_parameters(_login, _password) = [] (auto p, session& s, sqlite_connection& c)
  {
    if (!s.authenticate(c, p.login, p.password))
      throw error::bad_request("Invalid user or password");      
  },

As you can see in the procedure signature _login(_login, _password), it takes as argument the user login and password. By default, arguments have type string, but you can specify other argument types: _login(_login, _password = int()). The backends automatically deserialize the arguments and pass them to the procedure via in the auto parameter.

The procedure also takes as parameters two middleware instances: the session and the database connection. The framework is responsible of instantiating and passing the required middlewares to each procedure. We'll see later how to bind the middleware factories to the API.

The logout method just requires access to the session and call its logout method:

  GET / _logout = [] (session& s)
  {
    s.logout();
  },

We also need to manage our user table. The API relies on sql_crud, a generic CRUD generator, to generate 4 procedures (get_by_id, create, update, destroy) for the post and user objects.

It also provides some entry points to configure the behavior of the CRUD. I used 3 of them in this example:

I placed the CRUD routes under the user namespace. I'll show later how the backend reflects the namespaces in the routing scheme.

  _user = sql_crud<user_orm>(

    _before_create = [] (user& u, sqlite_connection& c) {
      if (!c("SELECT * from blog_users where login = ?")(u.login).empty())
        throw error::bad_request("User with login ", u.login, " already exists.");
      u.password = hash_sha3_512(u.password);
    },

    _validate = [] (user& u, sqlite_connection& c) {
      return u.login.size() > 0;
    },

    _write_access = [] (user& u, session& s) {
      return u.id == s.user_id;
    }

    ),

Like the user namespace, the post namespace manages the blog posts with sql_crud:

  _post = sql_crud<post_orm>(

    _validate = [] (post& p) {
      return p.title.size() > 0 and p.body.size() > 0;
    },

    _before_create = [] (post& p, session& s, restricted_area) {
      p.user_id = s.user_id;
    },

    _write_access = [] (post& p, session& s, restricted_area) {
      return p.user_id == s.user_id;
    }

    )

Middleware factories

We now need the factories required to instantiate the middlewares. restricted_area does not need a factory since it provides the instantiate static method. However, it is not the case of session, sqlite_connection, user_orm and post_orm.

auto middlewares = std::make_tuple(sqlite_connection_factory("blog.sqlite"),
                      user_orm_factory("blog_users"),
                      post_orm_factory("blog_posts"),
                      hashmap_session_factory<session>());

Serving the API via HTTP

The mhd_json_serve backend takes as input the API object and the port to listen to. It starts a HTTP server and serve the API via a routing scheme directly generated from the API hierarchy:

POST /login(login: string, password: string) -> void
GET /logout() -> void
GET /user/get_by_id(id: int) -> {id: int, login: string, password: string}
POST /user/create(login: string, password: string) -> {id: int}
POST /user/update(id: int, login: string, password: string) -> void
POST /user/destroy(id: int) -> void
GET /post/get_by_id(id: int) -> {id: int, user_id: int, title: string, body: string}
POST /post/create(title: string, body: string) -> {id: int}
POST /post/update(id: int, title: string, body: string) -> void
POST /post/destroy(id: int) -> void

To remotely call the API, the client passes the arguments via the body of the request. If an error occurs (i.e. an exception was thrown by a middleware or the procedure), a HTTP error is sent to the client. If the procedure runs successfully, the backend json-serializes its return value into the response body.

#include <silicon/backends/mhd.hh>
#include "blog_api.hh"

int main()
{
  sl::mhd_json_serve(blog_api, middlewares, 9999);
}