Creating Secure REST APIs in Node.js without JWTs

Before we go on creating an actual RESTful API, let's address the elephant in the room: how to make an HTTP endpoint sufficiently secure in Node.js ?

I say sufficiently because, the topic of security is broad and constantly evolves. This article is a response to other Node.js articles I've seen that contain security mistakes. It may not be perfect, either, but is hopefully a good evolution on the topic.

No JWT

Without JSON Web Token is a tongue-in-cheek statement which relates to some JWT obsession in Node.js community. JWTs don't bring anything new to the table compared to regular session IDs.

At the same time, JWTs come with additional complexity. They need to be well understood to prevent security mistakes, namely Cross-Site Scripting (XSS) attacks. It may be also difficult to invalidate such tokens (a case for stateless JWTs).

There is a bunch of interesting articles, presentations and videos, why you do not need JSON Web Tokens for a typical authentication in your web application. I highly recommend to read them up.

JWTs are more suited for claim-related situations. A typical usecase would a cloud storage application or a file hosting service. Users have to authenticate to browse and manage their files, but when they need to download a particular file, they get a single-use, short-lived token to access the file from another (asset) server; its role is only to provide files to download.

Also, for client-side websites, whether it is an old-school, server-side generated HTML or a single page-application (SPA), it is simpler to use HTTP cookies with properly set flags such as HttpOnly and SameSite. I will explore that topic in the context of Vue.js in the upcoming article.

In this article I'm exploring the usage of plain authentication tokens for securing REST APIs as a simpler alternative to JWTs.

Another Framework ?! Aaah...

The first step is to create a Node.js server which can receive requests and generate response. The most popular way of doing it, is to use an HTTP framework, such as Express.js.

In this article, I will try something different. My goal is to make the process of building a REST API simpler. That is why we will explore (yet) another framework called Huncwot. Yes, I know! I already see the scowl on your face. Please bear with me for the next few minutes. The reasons will be evident shortly.

While the general principles of securing endpoints are universal, Huncwot will allow us to be slightly more efficient by providing some helper functions out-of-the-box to reduce the boilerplate.

You shouldn't have any problems to adapt the code that follows to Express.js.

Warning: Please, use Node <= 11 (there is a bug with Node 12 as of 2019-05-04, it should be resolved shortly)

Let's create an empty directory for our RESTful API called secure-rest-api-nodejs.

mkdir secure-rest-api-nodejs

Inside, we put a file called app.js which will hold our entire, but still simple, Node.js application. Additionally, let's create package.json by executing npm init right inside that directory.

cd secure-rest-api-nodejs && touch app.js && npm init

Let's add the first dependency:

npm i huncwot --save

First Route

Our initial Node.js application is very simple. It has only one route, i.e. /, which returns a string of the type text/plain.

const Huncwot = require('huncwot');

const app = new Huncwot();

app.get('/', _ => "Hello, Huncwot!");

app.listen(5544);

Let's run it to see if works.

node app.js

Now, you can either open localhost:5544 in your browser, or you can use a command-line tool such as Wget, cURL or HTTPie. I prefer the CLI approach and HTTPie is one of my favourite tools. I can trigger a request using the following command:

http :5544/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 14
Content-Type: text/plain
Date: Fri, 01 Apr 2019 16:05:37 GMT

Hello, Huncwot

It works like a charm!

JSON Payload

RESTful APIs usually return JSON data. This translates to a response with the Contet-Type of application/json. Let's go ahead and fix that, so that our application returns JSON instead of plain strings. That's a quick fix in Huncwot. We can use the json() helper. It automatically serializes any JavaScript data structure into a JSON payload and properly sets the Content-Type as application/json.

In the following example, we return a simple JavaScript object with the single widget key:

const Huncwot = require('huncwot');
const { json } = require('huncwot/response');
const app = new Huncwot();

app.get('/', _ => json({ widget: "This is a widget available to everyone"}));
app.listen(5544);

Once you changed the app.js, you need to restart the whole application. It would be nice, if our application restarted automatically, when something changes within the application directory. Luckly, there is a tool just for that. It is called nodemon.

npm install nodemon --save-dev

The --save-dev option marks the dependency as only needed during the development. From now on, we can substitute node with nodemon to run the application. As we installed nodemon locally (the command is only available in the scope of this particular project), we need to use npx command (that comes included with Node.js) to run nodemon from the command line:

npx nodemon app.js

Let's repeat our request to / endpoint.

http :5544/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 29
Content-Type: application/json
Date: Fri, 01 Apr 2019 16:26:12 GMT

{
  "widget": "This is a widget available to everyone"
}

Remember that you can also open localhost:5544 in your browser for the same effect.

In order to streamline the development process even further, we can use the scripts section of package.json to simplify the command used to start our application. Let's add the start command as seen here:

{
  ...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app.js"  }
  ...
}

With that change in place, you can now use npm to start the application.

npm start
[nodemon] 1.18.10
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node app.js`

At this point, you may be curious about the structure of routes in Huncwot and what this weird _ (underscore) sign means. In Huncwot, you can define routes similar to Express or any other framework. You start by selecting one of the following functions: get, post, patch, put and delete. Each function refers to the corresponding HTTP request method.

Also, each of them takes two arguments. The first one is the path for this route. The second one is the function, which is responsible for handling any request mathing this particular path. By convention, this function is usually called a handler as it handles requests to produce responses.

A combination of a path and corresponding handler is a route.

This is, where things start to become different in Huncwot. Contrary to Express.js (and similar frameworks), a handler in Huncwot is a one argument function. This argument is the incoming request.

In Express, and the majority of other Node.js frameworks, handlers take two arguments. The first one is the request and the second one is the response. In Huncwot, the response is simply everything that is being returned by the handler. This way, it may be slightly more natural to think about the process of handling requests and generating responses.

In Huncwot, handlers are functions, which take requests as their input and produce responses as their output.

fn input output

The previous route could be rewritten in the following way:

const Huncwot = require('huncwot');
const { json } = require('huncwot/response');

const app = new Huncwot();

app.get('/', request => {   return json({ widget: "This is a widget available to everyone"}) });
app.listen(5544);

Since we don't need the incoming request data yet, the variable is not used. There is a practice to name unused function parameters as underscore (a practice especially common in the programming laguages from the ML family).

You can also configure your linter to skip those variables altogether, so this doesn't generate any errors if these variables are not used. Here is an example of a rule that achieves this effect in ESLint.

"rules": {
  "no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}]
}

On top of that, Huncwot serializes the incoming request data automatically: either headers, query parameters, the dynamic routes or the request body. All that must be explicitly configured in Express or similar solutions, which is probably tiny, but still an addititional burden.

Headers are available under request's headers variable while the last three are combined in the params field of the request argument. In case you don't need any other data from the incoming request, you can use ES6 destructuring assignment to access any part of data directly. Here is an example to showcase that approach:

app.get('/name/:name', ({ params }) => `Hello, your name is ${params.name}`);

And then you can trigger the following request:

http :5544/name/Zaiste
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 26
Content-Type: text/plain
Date: Fri, 05 Apr 2019 17:03:50 GMT

Hello, your name is Zaiste

This feature will come in handy later on, when we will be using the authentication token being passed via HTTP headers.

PostgreSQL for Persistance

In order to manage access to resources by making particular routes only available to logged in users, we need first to create entities that represent these users. We also need to store them in a database.

There are many options when it comes to databases. In this tutorial we will stick with a traditional (some may say boring), but solid solution for persistence, which is a relational data base. We will use PostgreSQL as our RDBMS.

Also, we won't be using any object relation mapping techniques. Knowing SQL is one the most valuable skills. Once you get a grip on it, it is not that complicated. When your application reaches a certain size, queries will become more streightforward to construct than fighting the constraines imposed by a particular ORM solution. Besides, it is not uncommon that ORM generated queries are significantly slower. In other words, using SQL is more flexible, efficient and simpler long-term, but not necessarily easier straightaway.

Table for Users... Person ?!

As user name is used by PostgreSQL for internal purposes. We will create a table called person to store the users of our application. I usually use the schema feature of PostgreSQL to be able to use the user keyword. This also provides some additional benefits. I won't be discussing this technique here for simplicity sake.

Notice, that I use singular form for the noun describing our entity. In some circles, this approach may be contested, but it is relatively common among database engineers. It simplifies your mental model in this context. You don't need to switch between forms or to be bothered by exceptions for certain words, e.g. leaf -> leaves, hero -> heroes, man -> men, etc

If you find it unusual, attach the word container to the name of any database relation e.g. person container, widget container. This mental trick may help you emphasis the role of a table as a container, or set, for things of a particular type.

Our person table will be short

create table person (
  id serial primary key, 
  name text, 
  email text, 
  password text
);

Let's place that CREATE TABLE query in the db.sql file within our project directory.

Start your PostgreSQL instance. There are many ways of doing it. Use the method available on your system, or refer to the PostgreSQL documentation to learn how to do it.

Create the database for our application called secure_rest_api_nodejs.

createdb secure_rest_api_nodejs

Notice that I'm using _ (underscore) and not - (hyphen/dash), which is an important distinction in PostgreSQL.

Finally, let's create the person table within the secure_rest_api_nodejs database by using psql

psql secure_rest_api_nodejs < db.sql

In most scenerios creating tables or changing the database schema should be performed via database migrations. Huncwot supports that too, but I'm intentionally skipping that part for simplicity reasons.

Register Route

At this point we haven't done anything related to securing our routes. Let's change that by introducing the /register route for creating (or registering) new users. Contrary to previous routes, this will be a route that responds only to POST requests as we will be sending user data to the server within the request body.

Our Node.js API will require three pieces of data to register a user: a name, an email and a password.

const Huncwot = require('huncwot');
const { json } = require('huncwot/response');

const app = new Huncwot();

app.get('/', _ => json({ widget: "This is a widget available to everyone"}));
app.post('/register', ({ params }) => {  const { name, email, password } = params;  let person = {    name,    email     password,  };    return json(person);})
app.listen(5544);

Notice, that I use object destructuring for the input parameter in the handler of the /register route as I'm only interested in request's params. Instead of writing request => { ... } I can just say ({ params }) => { ... }.

Then, using the same approch, I can extract only the request params that I'm interested in. In this case the three we defined initially. I assign it to person object and then I return that as JSON back as HTTP response. This last part is temporary for testing purposes.

Let's send some data via POST request to our newly create /register route.

http :5544/register name=Zaiste password=krzychujacielubie email=zaiste@example.com
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 113
Content-Type: application/json
Date: Fri, 01 Apr 2019 18:25:46 GMT

{
  "email": "zaiste@example.com",
  "name": "Zaiste",
  "password": "krzychujacielubie"
}

When using HTTPie I don't need to specify that this request should be sent as POST. By providing a list of key value parameters I implicitly indicate the need for a POST request.

This is slightly different than in similar tools such as cURL or Wget. In that case, you would need to specify the HTTP request method as POST explicitly. Probably, you would also need to set the request headers as JSON. If you don't know how to do it, either consult the documentation for your tool of choice, or check my Introduction to cURL; otherwise use HTTPie.

If you get back the data you sent in, as in the example above, it means the route works as expected. We can now finally implement the proper user registration. In order to do that, we will use Node.js bcrypt library to hash our passwords before storing them in the database. In short, a hash is a function that can easily transform data in our direction, but it is extremely difficult to transform that data back to its initial form. By storing hashed password in the database, we provide an additional layer of security. In an unlikely event of data breach, it is almost impossible to find passwords based on their hashes.

There are some Node.js articles out there that suggest using createHmac from Node.js crypto library for hashing passwords. You shouldn't do that. bcrypt function is specifically designed for hashing passwords. It provides the hash function that is intentionally slow (the concept of iterations) and salted: a salt is a different concept that a secret used for HMAC hashes.

const Huncwot = require('huncwot');
const { json } = require('huncwot/response');
const bcrypt = require('bcrypt');
const hash = bcrypt.hash;
app.get('/', _ => json({ widget: "This is a widget available to everyone"}));
app.post('/register', async ({ params }) => {
  const { name, email, password } = params;
  const hashedPassword = await hash(password, 12);
  let person = {
    name, 
    email, 
    password: hashedPassword  };

  return json({ hashedPassword });
});

We start by aliasing bcrypt.hash to hash for convenience. Then, we hash the received password using 12 rounds (which equals to 2^12 processing iterations). bcrypt.hash provides a Promise-based API js. Thus, we can use async/await syntax for additional clarity here. Finally, we return that hashed password as the response - something we do only for testing purposes.

No ORM, SQL-based Database API

In addition to the features mentioned previously, Huncwot provides a convenient API for the database access. It is thanks to a library called Sqorn that can build SQL queries out of JavaScript data structures. It is similar to Knex, but 10x faster and roughly 200x faster than Squel.

Here is an example of a Sqorn query:

sq.return({ authorId: 'a.id', name: 'a.last_name' })
  .distinct
  .from({ b: 'book' })
  .leftJoin({ a: 'author' }).on`b.author_id = a.id`
  .where({ title: 'Oathbringer',  genre: 'fantasy' })
  .query

Huncwot provides this API via the huncwot/db module. Let's use it to store registered users along with their hashed passwords.

const Huncwot = require('huncwot');
const { json } = require('huncwot/response');
const db = require('huncwot/db');const bcrypt = require('bcrypt');

const hash = bcrypt.hash;

app.get('/', _ => json({ widget: "This is a widget available to everyone"}));
app.post('/register', async ({ params }) => {
  const { name, email, password } = params;
  const hashedPassword = await hash(password, 10);

  let person = {
    name, 
    email, 
    password: hashedPassword
  };
  
  const [{ id: person_id }] = await db    .from('person')    .insert(person)    .return('id');
  return json({ person_id });
});

db helper allows to execute a SQL INSERT query with a properly arranged JavaScript object as its input. In our case, the input is the person object containing only the fields that match the columns in the person table.

This may be subjective, but it is not that far in terms of convenience when comparing with what ORMs usually provide is similar situations.

The only missing part is the database configuration. By default, the db helper looks for a configuration file within config/ (relative to the project). Let's add the following config as default.yml:

db: 
  database: secure_rest_api_nodejs
  username: zaiste

Attention, the username will be different for you.

Let's test it again:

http :5544/register name=Zaiste password=krzychujacielubie email=zaiste@example.com
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 48
Content-Type: application/json
Date: Fri, 01 Apr 2019 19:19:32 GMT

{
  "person_id": 1,
}

You should get back the database ID from the person table. Let's connect to our database to verify it:

psql secure_rest_api_nodejs
Null display is "¤".
Line style is unicode.
Border style is 1.
Expanded display is used automatically.
psql (11.2)
Type "help" for help.
[local] zaiste@secure_rest_api_nodejs = # 

And then, let's execute a SELECT query on the person table.

[local] zaiste@secure_rest_api_nodejs = # select * from person;

 id │  name  │      email             │                           password
────┼────────┼────────────────────────┼──────────────────────────────────────────────────────────────
  1 │ Zaiste │ zaiste@example.com     │ $2b$10$S/I1LIQlb2AaBOr56U1Y5.ClhRXSMHEs728pk3PJvurosG9EvHGj6
(1 row)

It looks like everything works as expected. Users are being created in the person table in response to POST requests. The password is stored in the database in the hashed form.

Authentication Token

The user registration is almost finished. We only miss the authentication token generation. This token will be exchanged between the client and the server as a way to manage the user session. In other words, any request containing this token will be treated as coming from a particular user. Based on that we will be able to grant access to certain endpoints or even distinguish between logged-in users using roles.

Authentication tokens should be at least 128 bits long and they should be generated from a cryptographically secure pseduo-random number generator (CPRNG).

In Node.js, we can use the randomBytes method from the crypto module. This method doesn't have a Promise-based API, so let's wrap it into a promise so to use it with the async/await syntax.

const fromBase64 = base64 =>
  base64
    .replace(/=/g, '')
    .replace(/\+/g, '-')
    .replace(/\//g, '_');

const token = await new Promise((resolve, reject) => {
  crypto.randomBytes(16, (error, data) => {
    error ? reject(error) : resolve(fromBase64(data.toString('base64')));
  });
});

Additionally, we need to transform the result from Base64 to Base64URL as there are certain characters which cannot be used in HTTP headers.

The token will be returned to the user, but we need to also store it in our database. Similar to passwords, it is a good idea to additionally hash it. This doesn't have to be a special hasing function as with the bcrypt library that is meant only for passwords. We can use any hash function that quickly generates the result.

const hashedToken = crypto
  .createHash('sha256')
  .update(token)
  .digest('base64');

We can now store the hashed token in the database connected with the corresponding user. We need to create an additional table called session for that.

create table session (
  id serial primary key, 
  token text, 
  person_id integer references person(id), 
  created_at timestamptz default now()
);

This table uses a foreign key to reference the corresponding row in the person table. This way you won't be able to insert a session for a user that doens't exist yet. created_at will be used for invalidating sessions. This field doesn't have to be specified when inserting data: it will always default to the current timestamp.

Finally, this is the /register route in its full gloary:

const Huncwot = require('huncwot');
const { json, created } = require('huncwot/response');
const db = require('huncwot/db');
const bcrypt = require('bcrypt');
const crypto = require('crypto');const { fromBase64 } = require('base64url');
const hash = bcrypt.hash;

app.get('/', _ => json({ widget: "This is a widget available to everyone"}));
app.post('/register', async ({ params }) => {
  const { name, email, password } = params;
  const hashedPassword = await hash(password, 10);

  let person = {
    name, 
    email, 
    password: hashedPassword
  };
  
  const [{ id: person_id }] = await db
    .from('person')
    .insert(person)
    .return('id');
    
  const token = await new Promise((resolve, reject) => {    crypto.randomBytes(16, (error, data) => {      error ? reject(error) : resolve(fromBase64(data.toString('base64')));    });  });
  const hashedToken = crypto    .createHash('sha256')    .update(token)    .digest('base64');
  await db`session`.insert({ token: hashedToken, person_id });
  return json({ token, person_id });});

We return the token after the registration process so that users can use the API right away. This is similar to automatically logging users in, once they finished with the registration.

Since we perform two writing operations on the database, it is a good idea to use transactions here. I'm skipping it intentionally for clarity sake.

Log In Route

Now, we need the /login route for a compelete authentication process. This route will allow users to generate a new authentication token without the need of registering. This will be similar to /register, but instead of creating a new user, we will try to find an existing user based on the incoming request data.

In this example, we require users to provide their email and password to log in.

const compare = bcrypt.compare;

app.post('/login', async ({ params }) => {
  const { email, password } = params;

  const [person] = await db.from('person')
    .where({ email });

  if (!person) return unauthorized();

  const match = await compare(password, person.password);

  if (!match) return unauthorized();
  
  const person_id = person.id;

  const token = await new Promise((resolve, reject) => {
    crypto.randomBytes(16, (error, data) => {
      error ? reject(error) : resolve(fromBase64(data.toString('base64')));
    });
  });

  const hashedToken = crypto
    .createHash('sha256')
    .update(token)
    .digest('base64');

  await db`session`.insert({ token: hashedToken, person_id });

  delete person.password;

  return json({ token, ...person }, { Authorization: token });
});

We start by aliasing bcrypt.compare to compare. This function checks if the hash generated from the value provided via password field matches the hash stored in the database for given user.

We identify the user via the email field. It is a SQL WHERE query that searches for users having this particular email. We only take the first result. In order to further constrain this case, we could mark the email column as unique in our database. There should be only one person with given email address.

If a person with given email is not found, we return a 401 Unauthorized response by using unauthorized() helper from huncwot/responses module. The same response is returned if the password doesn't match the one stored in the database. Otherwise, we generate the authentication token, we quickly hash it for storage and return that token to the user. This time we return in the response body AND in headers as Authorization.

In practice, the Authorization token is slightly different than a key/value pairs. The W3C introduced a type differentiatior to its value i.e.

Authorization: <type> <credentials>

Many servers support multiple methods of authorization. In those cases sending just the token isn't enough. There is a need to priovde the type field explicitly. In our case this is Bearer. There are other methods of HTTP authentication such as Basic or Digest. This logic is also automatically handled by Huncwot.

Securing Endpoints

We can finally secure some routes. A secure route will require a valid authentication token passed in the request to be accessed. Otherwise, we will return 401 Unauthorized response. In other words, a valid authentication token will make the handler finish without returning the 401 HTTP error.

Let's call our first secure route /secret:

app.get('/secret', async ({ headers, params }) => {
  const { authorization: token } = headers;

  if (!token) return unauthorized();

  const hash = crypto
    .createHash('sha256')
    .update(token)
    .digest('base64');

  const [found] = await db`session`({ token: hash });

  if (!found) return unauthorized();
  
  // from now on, we are properly authenticated

  return json({ secret: 'This message is only for admins!' });
});

This time, in addition to params, we also extract the headers from the incoming request as we intent to find the authentication token there as well. Then, we hash its value using the same function, we used before. This way we will be able to compare it with what is stored in the database. If the provided token matches the one stored in the database, we know that we can carry on and this is a properly authenticated request.

Additionally, once we found a valid session, we can also reference that particular user to check any other criteria, such roles, etc. I leave that part as an execrise for the reader.

Higher-Order Function Can?

You probably noticed that every secured route would need to perform the same steps at the beginning before doing the actual work. There is some repetition here that could be refactored. Luckly, Huncwot provides a helper just for that. It's called can() and it's a higher-order function (HOF). can takes a handler as its input and it returns a handler as its output. This way we can reduce previous authentication boilerplate code for each route that needs to be secured.

Let's simplify the previous snippet:

const { can } = require('huncwot/auth');

const secret = _ => 
  json({ secret: 'This message is only for admins!' });

app.get('/secret, can(secret)); 

Let's test if it all works together. First, register a new user.

http :5544/register name=Hanka email=hanka@agrobots.com password=tomliand1
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 48
Content-Type: application/json
Date: Sat, 01 Apr 2019 17:30:08 GMT

{
  "person_id": 2,
  "token": "c33qPZLwgb74V-ysAVu5Eg"
}

My authentication token is c33qPZLwgb74V-ysAVu5Eg - this value will be differnt for you.

Let see if I can access the /secret route without providing the token.

http :5544/secret
HTTP/1.1 401 Unauthorized
Connection: keep-alive
Content-Length: 0
Content-Type: text/plain
Date: Sat, 06 Apr 2019 17:30:40 GMT

I cannot access this route. Let's try doing the same request, but this time we will send our authentication token along with the request as an HTTP header.

http :5544/secure Authorization:c33qPZLwgb74V-ysAVu5Eg
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 45
Content-Type: application/json
Date: Sat, 01 Apr 2019 17:34:50 GMT

{
  "secret": "This message is only for admins!"
}

It works! We can successfully access the secured and secret route using the authentication token. This concludes the authentication process. We can now finish off by creating our RESTful API.

RESTful API at last

If you managed to reach this point, congratulations. It's been a long, but important introduction. We have now a pretty good base for securing routes at will in any Node.js application. Let's finish this article off by creating a simple Widget API.

There will be five CRUD routes:

  • a route for listing all widgets
  • a route for getting a particular widget by ID
  • a route for creating new widget
  • a route for updating an existing widget
  • finally, a route for deleting a widget

The routes that modify the data (which is creating, updating or deleting) will require an authentication token (they will be only available to registered users).

All widgets will be stored in the memory for simplicity reasons. It means that once the application is restarted, all the changes will be lost. I leave the implementation of persistance layer as the exercise for the reader.

const { json, created, ok, notFound } = require('huncwot/response');

const widgets = [
  {
    id: 1,
    name: "Widget 1"
  }
]

let _id = 1;  // counter: current id to assign to a new widget

const browse = _ => json(widgets);

const read = ({ params: { id } }) => {
  const widget = widgets.find(_ => _.id === +id)
  
  if (!widget) return notFound();
  
  return json(widget);
}

const add = ({ params: { name } }) => { 
  const id = ++_id;

  widgets.push({ id, name, })
  return created({ id, name })
}

const edit = ({ params: { id, name }}) => {
  const widget = widgets.find(_ => _.id === +id);
  
  widget.name = name;
  
  return ok();
}
 

app.get('/widgets', browse)
app.get('/widgets/:id', read)
app.post('/widgets', can(add))
app.patch('/widgets', can(edit))
app.delete('/widgets', can(destroy))

Bonus

I have been mentioning in this article, that there are many interesting features in Huncowt. So, what is there more?

In the context of authentication, the both routes we created at the beginning, i.e. /register & /login come built-in with Huncwot. This way you don't need to implement it each time for new applications.

Here is the final snippet for our secure Node.js RESTful API.

const Huncwot = require('huncwot');
const {
  ok,
  json,
  notFound,
  created,
  unauthorized
} = require('huncwot/response');
const { login, register, can } = require('huncwot/auth');
const db = require('huncwot/db');

// In-Memory State 

const widgets = [
  {
    id: 1,
    name: 'Widget 1'
  }
];

let _id = 1;

const app = new Huncwot();

// Handlers
const secret = _ => 
  json({ secret: 'This message is only for admins!' });

const browse = _ => 
  json(widgets);

const read = ({ params: { id } }) => 
  json(widgets.find(_ => _.id === +id));

const add = ({ params: { name } }) => {
  const id = ++_id;

  widgets.push({ id, name });

  return created({ id, name });
};

const edit = ({ params: { id, name } }) => {
  const widget = widgets.find(_ => _.id === +id);

  if (!widget) return notFound();

  widget.name = name;
  return json({ id, name });
};

const destroy = ({ params: { id } }) => {
  const widgetIndex = widgets.findIndex(_ => _.id === +id);

  if (widgetIndex < 0) return notFound();
  
  widgets.splice(widgetIndex, 1);

  return ok();
};

// Finder

const finder = async ({ email }) => {
  const result = await db.from('person').where({ email });
  return result;
};
 
// Routes

app.get('/', _ => 'Hello, Huncwot');
app.get('/name/:name', ({ params }) => `Hello, your name is ${params.name}`);
app.get('/json', _ => json({ widget: 'This is widget 1' }));

app.post('/register', register({ fields: ['name', 'email'] }));
app.post('/login', login({ finder }));

app.get('/secret', can(secret)); 
app.get('/widgets', browse);
app.get('/widgets/:id', read);
app.post('/widgets', can(add));
app.patch('/widgets/:id', can(edit));
app.delete('/widgets/:id', destroy);

app.listen(5544);

This code is also available on GitHub.

Huncwot provides many things out of the box, think Express.js with batteries included. If you want an integrated solution for building web applications that optimizes for programmers productivity by reducing choices and incorporating community conventions, look no further - Huncwot has your back. Be aware, though, the project is still in early stages.

Thanks to Peter Cooper & Piotr Kowalski for reviewing drafts of this article.

Copyright © 2019 All rights reserved.
Powered by Kulfon.