Building real time polling app using RethinkDB and Nodejs

A polling page becomes live when a vote reaches RethinkDB, the database emits the changed document, and a Node.js server forwards that event to connected browsers. This refresh builds that path with Express, the RethinkDB JavaScript driver, and Socket.IO, then shows where the example stops being a production design.

What the application does

The app stores each poll as one document with a question and an array of options. A browser reads the current collection through an HTTP route, sends a vote through another route, and receives the changed option through a Socket.IO event.

RethinkDB’s changefeed is the useful mechanism because the query remains open and the server receives a row when the watched table changes instead of asking the database for another snapshot on a timer.

Prerequisites and project setup

Install Node.js, npm, and a RethinkDB server using the current instructions in the RethinkDB installation documentation, then clone CodeForGeek’s polling app for the Express server, data model, views, and Socket.IO bridge.

git clone https://github.com/codeforgeek/polling-app-using-nodejs-rethinkDB.git
cd polling-app-using-nodejs-rethinkDB
npm install
node app.js

The repository’s package manifest requests the RethinkDB driver, Express, Socket.IO, body-parser, and async, and npm reported fourteen vulnerabilities in that legacy dependency tree during this refresh.

shahid at codeforgeek running npm install with the package audit result
The tested npm install command and its package audit output.

Start RethinkDB and create the table

The server connects to localhost on port 28015, creates the polls database, and creates its poll table, with an existing object treated as acceptable only after network and authentication errors are ruled out.

const r = require('rethinkdb');

r.connect({host: 'localhost', port: 28015, db: 'polls'}, (error, connection) => {
  if (error) throw error;

  r.table('poll').changes().run(connection, (feedError, cursor) => {
    if (feedError) throw feedError;
    cursor.each((rowError, change) => console.log(change));
  });
});

A changefeed is a cursor, not a one-time query result. Keep its connection open for as long as the server needs updates, and close the connection when the process shuts down.

Build the polling API

Express routes provide the three actions the browser needs. The GET route reads the current poll collection, the POST route inserts a question and its options, and the PUT route increments the selected option.

Create a poll

A new poll needs a question and a list whose items contain an option label and a vote count. The model inserts that object into the poll table, then closes its short-lived database connection.

r.table('poll').insert({
  question: pollData.question,
  polls: pollData.polls
}).run(connection, (error, result) => {
  connection.close();
  callback(error, result);
});

The insert result tells you whether RethinkDB accepted the document, but it does not replace input validation. Validate the question, require at least two options, and reject malformed vote counts before this query reaches the database.

Record a vote

The vote route first fetches the document by its id, finds the matching option, increments its count, and writes the changed document back. That sequence is easy to understand, but concurrent votes need a safer update strategy because two read-modify-write requests can observe the same old count.

r.table('poll').get(pollData.id).update(poll => ({
  polls: poll('polls').map(option =>
    option('option').eq(pollData.option)
      .branch(
        option.merge({vote: option('vote').add(1)}),
        option
      )
  )
})).run(connection, (error, result) => {
  connection.close();
  callback(error, result);
});

The update runs inside RethinkDB, so the server does not have to send the whole document to Node.js and back for each vote. Check the returned error and changed-row count, then report an unknown poll id or option as a client error instead of returning success.

Read current poll results

The GET route runs the poll table query and converts its cursor to an array before returning JSON. This snapshot is the browser’s starting state, while the changefeed supplies later state changes.

r.table('poll').run(connection, (error, cursor) => {
  if (error) {
    connection.close();
    return callback(error);
  }

  cursor.toArray((arrayError, polls) => {
    connection.close();
    callback(arrayError, polls);
  });
});

Turn database changes into live browser updates

When a browser opens a Socket.IO connection, the server starts a changefeed cursor for the poll table and receives the previous and new document for each change.

io.on('connection', socket => {
  r.table('poll').changes().run(connection, (error, cursor) => {
    if (error) return socket.emit('feedError', {message: error.message});

    cursor.each((rowError, change) => {
      if (rowError) return socket.emit('feedError', {message: rowError.message});
      if (!change.new_val) return;

      socket.broadcast.emit('changeFeed', {
        id: change.new_val.id,
        polls: change.new_val.polls
      });
    });
  });
});

The broadcast hands storage changes to the browser, which replaces the matching poll instead of polling the database.

Update the browser without polling

The client listens for the same changeFeed event and replaces the option counts for the matching id, while the initial HTTP response supplies the complete collection.

const socket = io();

socket.on('changeFeed', change => {
  const poll = polls.find(item => item.id === change.id);
  if (!poll) return;
  poll.polls = change.polls;
  renderPoll(poll);
});

socket.on('feedError', error => {
  showConnectionError(error.message);
});

A changefeed event can arrive before the client has a newly created document, so the client should request a fresh snapshot or ignore the event until its state includes the id.

Test the success path and failure boundaries

Open the app in two browser windows, create a poll, and vote in one window. The second window should receive the changed option count through Socket.IO without issuing another database read from the browser.

  • Confirm the RethinkDB server accepts a connection on port 28015.
  • Confirm GET returns the poll document before the first vote.
  • Confirm PUT increments the selected option and rejects an unknown option.
  • Confirm the changefeed cursor reports a new document after an insert or update.
  • Confirm a disconnected browser shows an error and can reload its snapshot.

If the app starts but no browser receives updates, inspect the database connection used by the feed first. A feed connection that closes after one query, a swallowed cursor error, or a client listening for a different event name breaks the path at a different boundary.

What the example does not solve

This sample does not include authentication, vote deduplication, TLS, Socket.IO adapter configuration for multiple server processes, or a reconnect policy for database outages. Add those decisions before exposing a voting endpoint to untrusted traffic.

It also uses a legacy dependency tree and an older AngularJS view layer. The changefeed idea remains the part worth carrying forward, while the client framework and deployment design should match the application you are building today.

Further reading and next move

Read the official JavaScript changefeed documentation next, especially its sections on initial values, filtered feeds, and cursor behavior. Then inspect the RethinkDB setup tutorial before changing the database connection or table lifecycle.

Run one insert and one vote while watching the changefeed cursor. If you can point to the document entering RethinkDB, the event leaving the cursor, and the browser state that changes, you understand the system you need to extend.

Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 336