Building a Solana Token Swap API with Node.js and Jupiter

·

Solana's high throughput and low transaction costs have made it a leading platform for decentralized applications, particularly in decentralized finance (DeFi). Among the most crucial DeFi functionalities is the ability to swap tokens efficiently. This comprehensive guide will walk you through building a robust backend API using Node.js that facilitates token swaps on the Solana blockchain by leveraging the Jupiter aggregation protocol.

You will learn how to set up a project, integrate with Solana's Web3 library, interact with Jupiter's API for quotes and swap execution, and structure your application for scalability. By the end of this tutorial, you will have a production-ready API endpoint capable of constructing token swap transactions.

Prerequisites and Initial Setup

Before diving into the code, ensure you have the following installed on your system:

Step 1: Initialize Your Node.js Project

Begin by creating a new project directory and initializing a Node.js project using Yarn. Open your terminal and execute the following commands:

mkdir jupiter-swap-api
cd jupiter-swap-api
yarn init -y

This sequence creates a new folder and generates a package.json file to manage your project's dependencies and scripts.

Step 2: Install Required Dependencies

Your API will rely on several key packages to function. Install them using Yarn:

yarn add @solana/web3.js @solana/spl-token @thewebchimp/primate dotenv

Step 3: Define the Project Structure

A well-organized project structure is vital for maintainability and future expansion. Create the following folders and files:

jupiter-swap-api/
│
├── app.js
├── controllers/
│ └── solana.controller.js
├── routes/
│ ├── default.js
│ └── solana.js
├── services/
│ └── solana.service.js
├── .env
├── package.json
└── test.http

This structure separates concerns: controllers handle request logic, services contain business logic, and routes manage API endpoints.

Developing the Core API Functionality

Step 4: Configure Environment Variables

Sensitive configuration details, such as RPC endpoints, should be stored in environment variables. Create a .env file in your project's root directory and add the following:

SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
JUPITER_QUOTE_API_URL=https://quote-api.jup.ag/v1/quote
JUPITER_SWAP_API_URL=https://swap-api.jup.ag/v1/swap

These variables point to the Solana mainnet and the Jupiter API endpoints for fetching quotes and executing swaps.

Step 5: Create the Application Entry Point

The app.js file serves as the starting point for your application. It initializes the server and registers your route handlers.

import primate from '@thewebchimp/primate';
import { router as solanaRouter } from './routes/solana.js';
import { router as defaultRouter } from './routes/default.js';

primate.setup();
await primate.start();

primate.app.use('/solana', solanaRouter);
primate.app.use('/', defaultRouter);

This code imports the Primate framework, sets up the server, and mounts the routers. The /solana path will handle all swap-related requests, while the root path provides a basic health check.

Step 6: Build the Solana Service Layer

The service layer (services/solana.service.js) contains the core logic for interacting with the Solana blockchain and Jupiter. It is responsible for validating inputs, fetching mint data, getting quotes, and constructing the swap transaction.

import { Connection, PublicKey } from '@solana/web3.js';
import { getMint } from '@solana/spl-token';
import 'dotenv/config';

const connection = new Connection(process.env.SOLANA_RPC_URL, 'confirmed');

class SolanaService {
  static async buildSwapTransaction(publicKey, inputMint, outputMint, amount, slippageBps = 0.5) {
    try {
      // Validate token mints exist on-chain
      const inputMintData = await getMint(connection, new PublicKey(inputMint));
      const outputMintData = await getMint(connection, new PublicKey(outputMint));

      if (!inputMintData || !outputMintData) {
        throw new Error('Invalid input or output mint');
      }

      // Validate slippage is within acceptable bounds
      if (slippageBps > 10) {
        throw new Error('Slippage must be less than 10%');
      }

      // Convert user-friendly amount to raw token amount using decimals
      const inputDecimals = inputMintData.decimals;
      const inputAmount = amount * Math.pow(10, inputDecimals);
      const slippagePercentage = slippageBps * 100;

      const walletPublicKey = new PublicKey(publicKey);

      // Construct the URL to fetch a quote from Jupiter
      let urlGet = `${process.env.JUPITER_QUOTE_API_URL}`;
      urlGet += `?inputMint=${inputMint}&outputMint=${outputMint}`;
      urlGet += `&amount=${inputAmount}&slippageBps=${slippagePercentage}`;
      urlGet += `&swapMode=ExactIn`;

      const quoteResponseData = await fetch(urlGet);
      const quoteResponse = await quoteResponseData.json();

      // Call Jupiter Swap API to construct the transaction
      const response = await fetch(`${process.env.JUPITER_SWAP_API_URL}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          dynamicComputeUnitLimit: true,
          prioritizationFeeLamports: 'auto',
          quoteResponse,
          userPublicKey: walletPublicKey.toString(),
          wrapAndUnwrapSol: true,
        }),
      });

      const jsonResponse = await response.json();

      if (jsonResponse?.error || !jsonResponse?.swapTransaction) {
        throw new Error(jsonResponse.error);
      }

      return jsonResponse.swapTransaction;
    } catch (error) {
      console.error('Error building swap transaction:', error);
      throw new Error(error.message);
    }
  }
}

export default SolanaService;

Step 7: Implement the API Controller

The controller (controllers/solana.controller.js) acts as the intermediary between the HTTP request and the service layer. It handles input validation and response formatting.

import SolanaService from '../services/solana.service.js';

class SolanaController {
  static async swapTokens(req, res) {
    const { inputMint, outputMint, amount, publicKey } = req.body;
    let { slippage } = req.body;

    // Validate all required parameters are present
    if (!inputMint || !outputMint || !amount || !publicKey) {
      return res.respond({
        status: 400,
        message: `Missing parameter: ${!inputMint ? 'inputMint' : !outputMint ? 'outputMint' : !amount ? 'amount' : 'publicKey'}`,
      });
    }

    // Set a default slippage of 0.5% if not provided
    if (!slippage) {
      slippage = 0.5;
    }

    try {
      // Delegate transaction building to the service layer
      const transaction = await SolanaService.buildSwapTransaction(publicKey, inputMint, outputMint, amount, slippage);
      res.respond({
        status: 200,
        data: { transaction },
        message: 'Swap transaction created successfully',
      });
    } catch (error) {
      // Handle errors gracefully
      res.respond({
        status: 500,
        message: error.message,
      });
    }
  }
}

export default SolanaController;

Step 8: Define the API Routes

Routes map incoming HTTP requests to the appropriate controller functions. Create two route files:

routes/solana.js for the swap functionality:

import { getRouter } from '@thewebchimp/primate';
import SolanaController from '../controllers/solana.controller.js';

const router = getRouter();
router.post('/swap', SolanaController.swapTokens);
export { router };

routes/default.js for a basic health check endpoint:

import { getRouter } from '@thewebchimp/primate';

const router = getRouter();
router.get('/', (req, res) => {
  res.respond({
    data: {
      message: 'Jupiter Swap API is operational!',
    },
  });
});
export { router };

Testing and Deployment

Step 9: Test Your API Endpoint

You can test your /solana/swap endpoint using an HTTP client like Postman or Thunder Client (VS Code extension). Create a test.http file to quickly send a POST request with the necessary JSON body.

POST http://localhost:1337/solana/swap
Content-Type: application/json

{
 "publicKey": "Your_Solana_Wallet_PublicKey",
 "inputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC mint
 "outputMint": "So11111111111111111111111111111111111111112", // SOL mint
 "amount": 1,
 "slippage": 0.5
}

A successful response will return a serialized transaction that can be signed and broadcasted to the network by a client-side wallet.

👉 Explore more strategies for transaction handling

Best Practices and Advanced Considerations

Frequently Asked Questions

What is the Jupiter Aggregator?
Jupiter is a liquidity aggregator on the Solana blockchain. It finds the best possible swap routes across all major decentralized exchanges (DEXs) within the ecosystem, ensuring users get the optimal price for their token swaps.

Why do I need to convert the token amount using decimals?
Blockchains handle tokens in their smallest unit (e.g., lamports for SOL, or base units for SPL tokens). Converting a user-friendly amount (e.g., 1.5 USDC) to its base unit (1500000 for USDC, which has 6 decimals) ensures the transaction is created with the precise value the blockchain expects.

What is slippage and why is it important?
Slippage is the difference between the expected price of a trade and the price at which the trade actually executes. In volatile markets, prices change rapidly. Slippage tolerance protects users from executing trades at unexpectedly bad prices. Setting it too low may cause transactions to fail, while setting it too high increases the risk of a unfavorable trade.

Can this API be used for a production application?
The code in this guide provides a solid foundation. For production use, you must enhance it with robust error handling, logging, monitoring, rate limiting, and thorough security audits. Always use dedicated private RPC services like QuickNode or Helius for better performance and reliability instead of the public mainnet endpoint.

Does this API execute the swap?
No. This API constructs the swap transaction. The serialized transaction it returns must be signed by the user's private key (this should never happen on the backend) and then broadcasted to the network. The signing step is always performed securely on the client-side, typically by a wallet like Phantom or Solflare.

How can I get a list of available token mints?
You can fetch a list of all SPL token mints from Solana ecosystem repositories or directly from Jupiter's API. Jupiter often provides an endpoint for fetching a list of tokens it supports for trading. 👉 Get advanced methods for token discovery

Conclusion

You have successfully built a Node.js API backend that interfaces with the Solana blockchain and Jupiter to create token swap transactions. This guide covered project initialization, dependency management, service layer logic, controller input handling, and route definition. This API serves as a powerful backend for any decentralized application requiring token swap functionality, from simple trading tools to complex DeFi platforms.

The principles learned here—interacting with blockchain RPCs, managing async operations, and structuring a scalable backend—are applicable to a wide range of Web3 development projects. Continue to experiment, add features, and integrate this API with a frontend to create a full-stack dApp.