dApp Backend Development with Node.js

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
dApp Backend Development with Node.js
Medium
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

DApp Backend Development with Node.js

Not every dApp needs a backend — that's the first thing to understand. If the logic is completely on-chain and the frontend reads data through a public RPC — the backend is redundant. But as soon as you need: private API keys, data aggregation from multiple sources, caching expensive on-chain queries, gasless transactions (relayer), signature verification — a backend becomes essential.

Key Components

RPC Abstraction Layer

Direct frontend calls to Infura/Alchemy expose the API key. The backend proxies RPC calls, adds caching and rate limiting:

import { JsonRpcProvider, Contract } from 'ethers';
import Fastify from 'fastify';

const provider = new JsonRpcProvider(process.env.RPC_URL);

const app = Fastify();

// Cached endpoint for contract data
app.get('/contract/:address/balance/:account', {
  config: { rateLimit: { max: 100, timeWindow: '1 minute' } }
}, async (req, reply) => {
  const { address, account } = req.params as { address: string; account: string };
  const cacheKey = `balance:${address}:${account}`;
  
  const cached = await redis.get(cacheKey);
  if (cached) return { balance: cached, cached: true };
  
  const contract = new Contract(address, ERC20_ABI, provider);
  const balance = await contract.balanceOf(account);
  
  await redis.setex(cacheKey, 12, balance.toString()); // cache for ~1 block (12 sec)
  return { balance: balance.toString(), cached: false };
});

Signature Verification (Authentication)

Sign-In With Ethereum (EIP-4361) is the standard for passwordless authentication. The user signs a SIWE message, the backend verifies the signature and issues a JWT:

import { SiweMessage } from 'siwe';
import jwt from 'jsonwebtoken';

app.post('/auth/verify', async (req, reply) => {
  const { message, signature } = req.body;
  
  const siweMessage = new SiweMessage(message);
  const result = await siweMessage.verify({ signature });
  
  if (!result.success) {
    return reply.code(401).send({ error: 'Invalid signature' });
  }
  
  const token = jwt.sign(
    { address: result.data.address, chainId: result.data.chainId },
    process.env.JWT_SECRET!,
    { expiresIn: '7d' }
  );
  
  return { token };
});

Nonce for replay attack protection: generate a random nonce, save it in Redis with a 5-minute TTL, verify that the nonce in the SIWE message matches the issued one. After use — delete it.

Event Indexing

On-chain events are needed to display transaction history, notifications, analytics. Two approaches:

Polling (simple): every N seconds query getLogs for recent blocks. It works, but with latency and consumes provider CUs.

WebSocket subscription (correct):

const wsProvider = new WebSocketProvider(process.env.WSS_RPC_URL);

const contract = new Contract(CONTRACT_ADDRESS, ABI, wsProvider);

contract.on('Transfer', async (from, to, value, event) => {
  await db.transfers.insert({
    from,
    to,
    value: value.toString(),
    blockNumber: event.log.blockNumber,
    txHash: event.log.transactionHash,
    timestamp: new Date(),
  });
  
  // Notify subscribers via WebSocket/SSE
  eventBus.emit('transfer', { from, to, value: value.toString() });
});

// Handle disconnection
wsProvider.on('error', async () => {
  console.error('WS disconnected, reconnecting...');
  setTimeout(setupSubscriptions, 5000);
});

WebSocket connections are unstable — reconnect logic is mandatory. Alternative for production: Alchemy webhooks, Quicknode Streams — the provider delivers events to your HTTP endpoint itself.

Gasless Transactions (Meta-Transactions)

EIP-2771 + ERC-2612 allow the user to sign a transaction off-chain while the relayer pays for gas. The backend acts as a relayer:

app.post('/relay/transfer', authenticateJWT, async (req, reply) => {
  const { permit, signature } = req.body; // ERC-2612 permit
  
  // Verify permit signature
  const tokenContract = new Contract(TOKEN_ADDRESS, ERC20_ABI, wallet);
  
  // Check that permit is valid and not expired
  const nonce = await tokenContract.nonces(permit.owner);
  if (BigInt(permit.nonce) !== nonce) {
    return reply.code(400).send({ error: 'Invalid nonce' });
  }
  
  // Execute permit + transferFrom on behalf of user
  const tx = await tokenContract.permit(
    permit.owner, permit.spender, permit.value,
    permit.deadline, permit.v, permit.r, permit.s
  );
  await tx.wait();
  
  return { txHash: tx.hash };
});

For production gasless transactions: OpenZeppelin Defender Relayer or Biconomy — they manage nonce, retry logic, and stuck transaction monitoring.

Project Structure

src/
  api/          # HTTP routes (Fastify/Express)
  blockchain/   # Provider, contracts, event listeners
  services/     # Business logic
  workers/      # BullMQ workers for background tasks
  db/           # Prisma schema, migrations
  cache/        # Redis client
  middleware/   # Auth, rate limiting, validation

Fastify is ~15-20% faster than Express in throughput and has built-in JSON schema validation. For a dApp backend, the difference is rarely critical, but the fastify-plugin ecosystem is convenient.

Monitoring and Reliability

Stuck transactions: a transaction with low gasPrice hangs in the mempool. Monitor via polling getTransactionReceipt(). After N minutes — bump gas (resend with same nonce, gasPrice * 1.1).

Nonce management: with parallel transactions from one wallet, you need an atomic nonce counter. Redis INCR + pending nonce tracking, or a library like ethers-multicall.

Circuit breaker for RPC: if the provider returns errors — switch to a backup. Implement circuit breaker pattern via opossum or manually.

Development Timeline

Basic backend (RPC proxy + SIWE auth + caching) — 2-3 days. Event indexer + WebSocket push + gasless relay — another 3-4 days. Production-ready with monitoring, retry logic, and fallback RPC — 1.5-2 weeks.