Abhisekhkotnala/voting_backend: The Express Middleware That Turns Smart Contracts into REST

A compact Node.js bridge that hides ABI complexity, signs owner transactions on the server, and exposes an Ethereum voting contract as a conventional API.

7 min read • View on GitHub • More from Abhisekhkotnala

A wide editorial scene showing a REST console on one side and an Ethereum contract vault on the other, with a narrow corridor in between where requests are transformed into signed transactions. Read-only slips pass through untouched, while write requests are stamped and sealed before entering the chain side. The image explains the repo’s main idea: a backend that translates Web2 requests into Web3 actions.
The repo is less a voting app than a translation layer. It decides which requests stay simple JSON and which ones need signing, gas, and chain-aware execution.
Key Takeaways

The hidden job of this backend

The most interesting thing in Abhisekhkotnala/voting_backend is not the voting domain. It is the translation layer. This Express app sits between a regular client and an Ethereum contract, and it decides what can be treated like a normal JSON request and what needs blockchain-specific handling.

That is the real pattern here. Read calls become clean API responses. Write calls become signed transactions. ABI details, gas, and provider setup disappear behind the server, which is exactly why this repo reads like a bridge between Web2 habits and Web3 mechanics.

One server, two trust paths. The diagram makes the split visible: read requests flow through as queries, while write requests cross a signing boundary before they touch the contract.

Why a voting DApp needs an Express server at all

Frontends do not want to manage ABI encoding, RPC configuration, or the odd edges of transaction flow. They want endpoints. They want predictable JSON. They want the blockchain to feel like any other dependency until a real write has to happen.

Traditional REST backendThis Web3-aware backend
Server signs nothing specialServer may sign admin writes with an owner key
Client only knows JSONClient must understand which actions are read and which are chain writes
Persistence stays in one databaseState lives in a smart contract, with the backend mediating access
Failure modes are mostly HTTP and DB errorsFailure modes include RPC issues, gas, wallet state, and chain reverts
Best for ordinary CRUD appsBest for DApps that need a normal API surface over contract logic

That is why this repo is useful. It is not trying to replace the contract. It is trying to make the contract usable from a conventional app without forcing every client to become a Web3 client.

The signing split: server-owned actions vs user-driven votes

The core technical decision is simple and easy to miss. Some actions are server-owned. Others are user-driven. In this repo, addCandidate is guarded by ownerAuth and signed with OWNER_PRIVATE_KEY. getCandidates and getWinner use .call(), which stays read-only. castVote uses .send({ from: address }), which assumes the caller is already able to act through the configured chain environment.

A close-up technical illustration of two request paths branching from the same Express server. One path goes through an owner authentication gate, then into a wallet node, then into a signed admin transaction. The other path goes directly from a voter request into a contract write path and out as a vote receipt. The image explains the trust split between admin actions and user voting.
The repo’s most important idea is the split in trust. Admin actions are server-signed. Vote actions follow a separate path that assumes the chain side can accept the sender identity.

That split is elegant in a local demo because it keeps the server in control of privileged actions. It is also where the real assumption lives. The backend is treating some identities as trusted enough to sign for them, which is fine for a tutorial or a private network, but much less comfortable once real users and real value enter the picture.

Inside the codebase: config, controllers, routes, middleware

The repository uses a clean controller-route-middleware structure. That matters because blockchain glue code gets messy fast. Here, the contract instance lives in src/config/, the business logic sits in src/controllers/, route definitions stay thin, owner checks live in middleware, and error handling is centralized instead of scattered across handlers.

// src/controllers/voting.controller.js
export const getCandidates = async (req, res, next) => {
  try {
    const candidates = await contract.methods.getCandidates().call();
    const normalized = candidates.map((c) => ({
      name: c.name,
      voteCount: Number(c.votes),
    }));
    res.json(normalized);
  } catch (error) {
    next(error);
  }
};

export const addCandidate = async (req, res, next) => {
  try {
    const { name } = req.body;
    await contract.methods.addCandidate(name).send({ from: ownerAddress });
    res.json({ success: true });
  } catch (error) {
    next(error);
  }
};

Two details stand out. First, web3.eth.accounts.wallet.add makes the owner key available to the backend for signing. Second, the code normalizes contract output into plain JavaScript objects, which is exactly the kind of boring transformation that makes an API pleasant to use.

That boring part is the point. The repo is not showing off blockchain novelty. It is smoothing the edges so a normal client does not have to think about EVM-shaped data every time it asks for candidates or the winner.

The security story is the real story

The middleware is named like a simple gate, and that is what it is. ownerAuth checks a shared secret in the request body against OWNER_PRIVATE_KEY in the environment. For a teaching repo, that is understandable. For production, it is a red flag if you treat it as a general pattern.

PatternGood forWeakness
Shared-secret owner middlewareTutorials and local demosLeaks trust into request bodies and couples auth to a private key
WalletConnect or user signaturesReal users with separate identitiesMore moving parts and more UI work
RPC-unlocked sender accountsLocal dev chainsNot suitable for public deployments
Role-based server signingControlled admin workflowsRequires careful key management and auditability

The repo teaches by exposing the compromise, not by hiding it. That is a strength. You can see exactly where the trust boundary is, which is more useful than a polished demo that pretends blockchain authentication is solved by default.

From prototype_server.js to a modular server

The presence of a prototype_server.js alongside the modular src/ layout tells a familiar story. First comes the monolith. Then comes the refactor. In this case, the refactor is not just about prettier files. It creates sharper responsibility boundaries and makes the code safer to change.

Prototype serverModular server
Everything in one fileConfig, controllers, routes, middleware, and utils are separated
Fast to sketch, hard to extendEasier to test and reason about
Security and transport logic mixed togetherTrust checks live in middleware where they belong
Good for proving the ideaBetter for maintaining the idea

That evolution is the final lesson. The project starts as a bridge, but the modular version turns it into an actual system. Once responsibilities are separated, the backend stops being a script that talks to a contract and starts looking like a small but coherent platform layer.