← Back to Blog
Building Blockchain-Integrated Applications with Node.js

At Verizon, I developed a full-stack NFT collectibles solution for AMC's Walking Dead franchise. This wasn't just another blockchain project - it was a production system that had to handle real users, real transactions, and real money. Here's what I learned.

The Architecture Challenge

Blockchain interactions are fundamentally different from traditional API calls. They're slow, expensive, and can fail in unpredictable ways. We needed an architecture that could:

  1. Handle blockchain latency without blocking users
  2. Manage transaction costs (gas fees)
  3. Provide a responsive user experience
  4. Scale to thousands of concurrent users

The Request Queue System

Our solution was a request scheduling system using Redis and Socket.io:

// blockchain-queue.service.ts
import Redis from 'ioredis';
import { Server as SocketServer } from 'socket.io';
import { ethers } from 'ethers';
 
interface BlockchainRequest {
  id: string;
  type: 'mint' | 'transfer' | 'query';
  userId: string;
  params: any;
  priority: number;
  timestamp: number;
}
 
export class BlockchainQueueService {
  private redis: Redis;
  private io: SocketServer;
  private provider: ethers.providers.Provider;
  private contract: ethers.Contract;
  private processing = false;
  private maxConcurrent = 3;
  private activeRequests = 0;
 
  constructor(
    redisClient: Redis,
    socketServer: SocketServer,
    contractAddress: string,
    contractABI: any
  ) {
    this.redis = redisClient;
    this.io = socketServer;
 
    this.provider = new ethers.providers.JsonRpcProvider(
      process.env.ETHEREUM_RPC_URL
    );
 
    this.contract = new ethers.Contract(
      contractAddress,
      contractABI,
      this.provider
    );
  }
 
  async enqueueRequest(request: Omit<BlockchainRequest, 'id' | 'timestamp'>): Promise<string> {
    const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 
    const fullRequest: BlockchainRequest = {
      id: requestId,
      timestamp: Date.now(),
      ...request
    };
 
    // Add to sorted set by priority
    await this.redis.zadd(
      'blockchain_queue',
      fullRequest.priority,
      JSON.stringify(fullRequest)
    );
 
    // Start processing if not already running
    if (!this.processing) {
      this.processQueue();
    }
 
    return requestId;
  }
 
  private async processQueue(): Promise<void> {
    this.processing = true;
 
    while (await this.redis.zcard('blockchain_queue') > 0) {
      // Wait if we're at max concurrent requests
      while (this.activeRequests >= this.maxConcurrent) {
        await new Promise(resolve => setTimeout(resolve, 100));
      }
 
      // Get highest priority request
      const items = await this.redis.zpopmax('blockchain_queue');
 
      if (!items || items.length === 0) continue;
 
      const [requestJson] = items;
      const request: BlockchainRequest = JSON.parse(requestJson);
 
      // Process request without awaiting (handle concurrently)
      this.executeRequest(request);
    }
 
    this.processing = false;
  }
 
  private async executeRequest(request: BlockchainRequest): Promise<void> {
    this.activeRequests++;
 
    try {
      let result;
 
      switch (request.type) {
        case 'mint':
          result = await this.mintNFT(request.params);
          break;
        case 'transfer':
          result = await this.transferNFT(request.params);
          break;
        case 'query':
          result = await this.queryNFT(request.params);
          break;
        default:
          throw new Error(`Unknown request type: ${request.type}`);
      }
 
      // Emit success to specific user
      this.io.to(request.userId).emit(`request_${request.id}`, {
        status: 'success',
        data: result
      });
 
      // Store result in Redis for 24 hours
      await this.redis.setex(
        `result_${request.id}`,
        86400,
        JSON.stringify({ status: 'success', data: result })
      );
 
    } catch (error) {
      console.error(`Request ${request.id} failed:`, error);
 
      this.io.to(request.userId).emit(`request_${request.id}`, {
        status: 'error',
        error: error.message
      });
 
      await this.redis.setex(
        `result_${request.id}`,
        86400,
        JSON.stringify({ status: 'error', error: error.message })
      );
    } finally {
      this.activeRequests--;
    }
  }
 
  private async mintNFT(params: { to: string; tokenId: string; metadata: string }): Promise<any> {
    const wallet = new ethers.Wallet(
      process.env.MINTING_PRIVATE_KEY,
      this.provider
    );
 
    const contractWithSigner = this.contract.connect(wallet);
 
    // Estimate gas
    const gasLimit = await contractWithSigner.estimateGas.mint(
      params.to,
      params.tokenId,
      params.metadata
    );
 
    // Add 20% buffer
    const gasLimitWithBuffer = gasLimit.mul(120).div(100);
 
    const tx = await contractWithSigner.mint(
      params.to,
      params.tokenId,
      params.metadata,
      {
        gasLimit: gasLimitWithBuffer
      }
    );
 
    // Wait for confirmation
    const receipt = await tx.wait(2); // Wait for 2 confirmations
 
    return {
      transactionHash: receipt.transactionHash,
      blockNumber: receipt.blockNumber,
      tokenId: params.tokenId
    };
  }
 
  private async transferNFT(params: { from: string; to: string; tokenId: string }): Promise<any> {
    const tx = await this.contract.transferFrom(
      params.from,
      params.to,
      params.tokenId
    );
 
    const receipt = await tx.wait(2);
 
    return {
      transactionHash: receipt.transactionHash,
      blockNumber: receipt.blockNumber
    };
  }
 
  private async queryNFT(params: { tokenId: string }): Promise<any> {
    const owner = await this.contract.ownerOf(params.tokenId);
    const tokenURI = await this.contract.tokenURI(params.tokenId);
 
    // Fetch metadata
    const metadata = await fetch(tokenURI).then(res => res.json());
 
    return {
      tokenId: params.tokenId,
      owner,
      metadata
    };
  }
}

API Endpoints

The REST API provided a simple interface for the frontend:

// blockchain.controller.ts
import { Request, Response } from 'express';
import { BlockchainQueueService } from './blockchain-queue.service';
 
export class BlockchainController {
  constructor(private queueService: BlockchainQueueService) {}
 
  async mintNFT(req: Request, res: Response) {
    const { to, metadata } = req.body;
    const userId = req.user.id;
 
    try {
      const tokenId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
 
      const requestId = await this.queueService.enqueueRequest({
        type: 'mint',
        userId,
        params: { to, tokenId, metadata },
        priority: 10 // High priority for minting
      });
 
      res.json({
        requestId,
        message: 'Minting request queued. You will be notified when complete.',
        estimatedTime: '2-5 minutes'
      });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
 
  async getUserNFTs(req: Request, res: Response) {
    const { address } = req.params;
 
    try {
      const requestId = await this.queueService.enqueueRequest({
        type: 'query',
        userId: req.user.id,
        params: { address },
        priority: 1 // Lower priority for queries
      });
 
      res.json({
        requestId,
        message: 'Query queued'
      });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
 
  async getRequestStatus(req: Request, res: Response) {
    const { requestId } = req.params;
 
    try {
      const result = await this.queueService.getRequestResult(requestId);
 
      if (!result) {
        res.json({ status: 'pending' });
      } else {
        res.json(result);
      }
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  }
}

React Native Frontend

The mobile app used Socket.io for real-time updates:

// useBlockchainRequest.ts
import { useState, useEffect } from 'react';
import io from 'socket.io-client';
 
export function useBlockchainRequest(requestId: string | null) {
  const [status, setStatus] = useState<'pending' | 'success' | 'error'>('pending');
  const [data, setData] = useState<any>(null);
  const [error, setError] = useState<string | null>(null);
 
  useEffect(() => {
    if (!requestId) return;
 
    const socket = io(process.env.API_URL);
 
    socket.on(`request_${requestId}`, (response) => {
      if (response.status === 'success') {
        setStatus('success');
        setData(response.data);
      } else {
        setStatus('error');
        setError(response.error);
      }
    });
 
    return () => {
      socket.disconnect();
    };
  }, [requestId]);
 
  return { status, data, error };
}
 
// MintNFTScreen.tsx
export function MintNFTScreen() {
  const [requestId, setRequestId] = useState<string | null>(null);
  const { status, data, error } = useBlockchainRequest(requestId);
 
  const handleMint = async () => {
    const response = await fetch(`${API_URL}/blockchain/mint`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`
      },
      body: JSON.stringify({
        to: userAddress,
        metadata: nftMetadata
      })
    });
 
    const { requestId } = await response.json();
    setRequestId(requestId);
  };
 
  return (
    <View>
      <Button title="Mint NFT" onPress={handleMint} />
 
      {status === 'pending' && (
        <ActivityIndicator size="large" />
      )}
 
      {status === 'success' && (
        <Text>NFT Minted! Transaction: {data.transactionHash}</Text>
      )}
 
      {status === 'error' && (
        <Text>Error: {error}</Text>
      )}
    </View>
  );
}

Key Lessons

  1. Never block the UI: Blockchain operations are slow - always queue them
  2. Gas estimation is critical: Running out of gas wastes money
  3. Confirmations matter: Wait for multiple confirmations for important transactions
  4. Error handling is complex: Network issues, gas issues, smart contract reverts - handle them all
  5. Test on testnet extensively: Real ETH is expensive - test thoroughly first

Conclusion

Blockchain development requires rethinking traditional application architecture. The key is building systems that gracefully handle the asynchronous, expensive nature of blockchain interactions while providing a great user experience.

The queue-based approach we built at Verizon scaled well and kept users happy even during high load periods.

Share this article

Help others discover this content


Jason Cochran

Jason Cochran

Sofware Engineer | Cloud Consultant | Founder at Strataga

27 years of experience building enterprise software for oil & gas operators and startups. Specializing in SCADA systems, field data solutions, and AI-powered rapid development. Based in Midland, TX serving the Permian Basin.