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.
- This repo is best understood as a Web3 gateway, not a voting app, because it turns contract calls into familiar REST endpoints.
- The important design choice is the split between read-only queries and signed write paths, which makes the backend act like a transaction broker.
- The code’s folder structure matters because it separates blockchain config, controller logic, middleware checks, and error handling into clear responsibilities.
- The project is educational on purpose, and its security model shows exactly where a simple teaching pattern stops being production-ready.
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.
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 backend | This Web3-aware backend |
|---|---|
| Server signs nothing special | Server may sign admin writes with an owner key |
| Client only knows JSON | Client must understand which actions are read and which are chain writes |
| Persistence stays in one database | State lives in a smart contract, with the backend mediating access |
| Failure modes are mostly HTTP and DB errors | Failure modes include RPC issues, gas, wallet state, and chain reverts |
| Best for ordinary CRUD apps | Best 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.
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.
| Pattern | Good for | Weakness |
|---|---|---|
| Shared-secret owner middleware | Tutorials and local demos | Leaks trust into request bodies and couples auth to a private key |
| WalletConnect or user signatures | Real users with separate identities | More moving parts and more UI work |
| RPC-unlocked sender accounts | Local dev chains | Not suitable for public deployments |
| Role-based server signing | Controlled admin workflows | Requires 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 server | Modular server |
|---|---|
| Everything in one file | Config, controllers, routes, middleware, and utils are separated |
| Fast to sketch, hard to extend | Easier to test and reason about |
| Security and transport logic mixed together | Trust checks live in middleware where they belong |
| Good for proving the idea | Better 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.