GitHub PR & Issue Viewer: Node.js Proxy Guide

by Alex Johnson 46 views

Ever found yourself staring at your GitHub repository, wishing you could easily browse through all the Pull Requests (PRs) and Issues without getting bogged down by CORS errors or complex setups? Well, you're in luck! In this guide, we're going to build an interactive tool that lets you do just that. We'll be using the power of Node.js to create a local server that acts as a friendly proxy to the GitHub API. This means no more frustrating CORS issues, just a smooth, direct way to fetch and view your repository's PRs and Issues right in your browser. We'll walk you through each step, from setting up your Node.js server to creating a slick user interface. Get ready to supercharge your GitHub workflow!

Setting Up Your Node.js Proxy Server

First things first, let's get our Node.js server up and running. This server will be the unsung hero, silently handling all the requests to the GitHub API on our behalf. This is crucial because direct calls from your browser to the GitHub API can often be blocked due to Cross-Origin Resource Sharing (CORS) policies. By routing these requests through our local Node.js server, we effectively bypass these restrictions, making our frontend application behave as if it were on the same origin as the GitHub API. To get started, you'll need Node.js installed on your machine. If you don't have it, a quick visit to the official Node.js website will get you sorted. Once Node.js is installed, we'll use Express.js, a minimalist web application framework for Node.js, to build our server. Create a new directory for your project, navigate into it using your terminal, and initialize a new Node.js project with npm init -y. Then, install Express with npm install express. Now, create a file named server.js and let's start coding.

const express = require('express');
const axios = require('axios'); // We'll use axios to make HTTP requests to the GitHub API
const app = express();
const port = 3000; // Or any port you prefer

// Middleware to parse JSON bodies
app.use(express.json());

// CORS middleware (for development purposes, allow all origins)
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
  next();
});

// Helper function to fetch data from GitHub API
const fetchGitHubData = async (apiUrl, res) => {
  try {
    const response = await axios.get(apiUrl, {
      headers: {
        // It's good practice to include a User-Agent
        'User-Agent': 'Node.js GitHub Viewer'
      }
    });
    res.json(response.data);
  } catch (error) {
    console.error('Error fetching data from GitHub:', error.response ? error.response.data : error.message);
    res.status(error.response ? error.response.status : 500).json({
      message: 'Failed to fetch data from GitHub',
      error: error.response ? error.response.data : error.message
    });
  }
};

// Route to get Pull Requests
app.get('/api/prs', async (req, res) => {
  const repo = req.query.repo;
  if (!repo) {
    return res.status(400).json({ message: 'Repository parameter (repo=user/repo) is required.' });
  }
  const apiUrl = `https://api.github.com/repos/${repo}/pulls`;
  await fetchGitHubData(apiUrl, res);
});

// Route to get Issues
app.get('/api/issues', async (req, res) => {
  const repo = req.query.repo;
  if (!repo) {
    return res.status(400).json({ message: 'Repository parameter (repo=user/repo) is required.' });
  }
  const apiUrl = `https://api.github.com/repos/${repo}/issues`;
  await fetchGitHubData(apiUrl, res);
});

app.listen(port, () => {
  console.log(`Node.js proxy server running at http://localhost:${port}`);
});

In this server.js file, we've set up an Express application. We've included a basic CORS middleware to allow requests from any origin, which is fine for local development but should be more restricted in a production environment. The core of our server lies in the two GET routes: /api/prs and /api/issues. Both routes expect a repo query parameter, which should be in the format user/repo. They then construct the appropriate GitHub API URL and use axios to fetch the data. The fetched data is then sent back to the frontend as JSON. Error handling is included to catch issues during the API call and to send back informative error messages to the client. To run this server, simply open your terminal in the project directory and type node server.js. You should see the message indicating that the server is running. This server will now act as our intermediary, allowing our frontend application to request GitHub data without encountering CORS restrictions. This setup is foundational for our interactive viewer, ensuring a seamless data retrieval process.

Building the Frontend: User Input and Data Selection

With our Node.js proxy server humming along, it's time to build the frontend interface that users will interact with. This is where the magic of fetching and displaying GitHub data comes to life. We'll create a simple HTML structure with the necessary UI components: an input field for the GitHub repository URL, a dropdown to select whether to fetch Pull Requests or Issues, and another dropdown that will dynamically populate with the fetched data. For simplicity, we'll use plain JavaScript to handle the interactions and fetch API to communicate with our Node.js proxy server. Let's set up our index.html file.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>GitHub PR/Issue Viewer</title>
    <style>
        body { font-family: sans-serif; margin: 20px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input, select, button { margin-bottom: 15px; padding: 8px; border-radius: 4px; border: 1px solid #ccc; }
        #detailsPanel { margin-top: 20px; border-top: 1px solid #eee; padding-top: 15px; }
        .error { color: red; font-weight: bold; }
    </style>
</head>
<body>
    <h1>GitHub PR & Issue Viewer</h1>

    <div>
        <label for="repoUrl">GitHub Repository URL:</label>
        <input type="text" id="repoUrl" placeholder="e.g., https://github.com/user/repo">
    </div>

    <div>
        <label for="dataType">Select Data Type:</label>
        <select id="dataType">
            <option value="prs">Pull Requests</option>
            <option value="issues">Issues</option>
        </select>
    </div>

    <button id="fetchButton">Fetch Data</button>

    <div id="errorMessage" class="error"></div>

    <div>
        <label for="itemSelector">Select PR/Issue:</label>
        <select id="itemSelector" disabled>
            <option value="">-- Select an Item --</option>
        </select>
    </div>

    <div id="detailsPanel">
        <h2>Details</h2>
        <div id="itemDetails">
            <p>Select an item from the dropdown to see its details.</p>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

And here's the corresponding script.js to handle the logic:

document.addEventListener('DOMContentLoaded', () => {
    const repoUrlInput = document.getElementById('repoUrl');
    const dataTypeSelect = document.getElementById('dataType');
    const fetchButton = document.getElementById('fetchButton');
    const itemSelector = document.getElementById('itemSelector');
    const itemDetailsDiv = document.getElementById('itemDetails');
    const errorMessageDiv = document.getElementById('errorMessage');

    const GITHUB_PROXY_URL = 'http://localhost:3000/api/'; // Our Node.js proxy

    // Function to validate GitHub URL format
    const isValidRepoUrl = (url) => {
        const pattern = /^https:\/\/github\.com\/[^\/]+\/[^\/]+$/;
        return pattern.test(url);
    };

    // Function to extract user/repo from URL
    const getRepoPath = (url) => {
        try {
            const urlObj = new URL(url);
            const pathParts = urlObj.pathname.split('/').filter(Boolean);
            if (pathParts.length === 2) {
                return `${pathParts[0]}/${pathParts[1]}`;
            }   
        } catch (e) {
            // Handle invalid URL
        }
        return null;
    };

    // Fetch data and populate the second dropdown
    const fetchData = async () => {
        const url = repoUrlInput.value.trim();
        errorMessageDiv.textContent = ''; // Clear previous errors
        itemSelector.innerHTML = '<option value="">-- Select an Item --</option>'; // Reset selector
        itemSelector.disabled = true;
        itemDetailsDiv.innerHTML = '<p>Select an item from the dropdown to see its details.</p>'; // Reset details

        if (!isValidRepoUrl(url)) {
            errorMessageDiv.textContent = 'Invalid GitHub repository URL format. Use https://github.com/user/repo';
            return;
        }

        const repoPath = getRepoPath(url);
        const type = dataTypeSelect.value;
        const apiUrl = `${GITHUB_PROXY_URL}${type}?repo=${repoPath}`;

        try {
            const response = await fetch(apiUrl);
            if (!response.ok) {
                const errorData = await response.json();
                throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
            }
            const data = await response.json();
            
            // Populate the item selector dropdown
            data.forEach(item => {
                const option = document.createElement('option');
                option.value = item.number; // Use number as value for easy lookup
                option.textContent = `#${item.number}: ${item.title}`;
                itemSelector.appendChild(option);
            });
            itemSelector.disabled = false;

        } catch (error) {
            console.error('Error fetching data:', error);
            errorMessageDiv.textContent = `Failed to fetch ${type}: ${error.message}`;
        }
    };

    // Display details of the selected PR/Issue
    const showDetails = async () => {
        const selectedItemId = itemSelector.value;
        if (!selectedItemId) {
            itemDetailsDiv.innerHTML = '<p>Select an item from the dropdown to see its details.</p>';
            return;
        }

        const url = repoUrlInput.value.trim();
        const repoPath = getRepoPath(url);
        const type = dataTypeSelect.value;
        // For issues and PRs, the GitHub API uses the same endpoint structure for details
        // We need to find the specific item in the previously fetched data or re-fetch it
        // For simplicity, let's re-fetch here or assume we store the data globally
        
        // A more efficient approach would be to store the fetched 'data' from fetchData()
        // and find the item by selectedItemId. For now, let's simulate fetching detail.
        // In a real app, you'd fetch a more specific endpoint or use cached data.
        
        // Let's assume we want to fetch the specific item details
        // For PRs, the endpoint is /repos/:owner/:repo/pulls/:pull_number
        // For Issues, the endpoint is /repos/:owner/:repo/issues/:issue_number
        
        let detailApiUrl;
        if (type === 'prs') {
            detailApiUrl = `${GITHUB_PROXY_URL}repos/${repoPath}/pulls/${selectedItemId}`;
        } else { // issues
            detailApiUrl = `${GITHUB_PROXY_URL}repos/${repoPath}/issues/${selectedItemId}`;
        }

        try {
            const response = await fetch(detailApiUrl);
            if (!response.ok) {
                const errorData = await response.json();
                throw new Error(errorData.message || `HTTP error! status: ${response.status}`);
            }
            const item = await response.json();

            // Render details
            itemDetailsDiv.innerHTML = `
                <h3>${item.title}</h3>
                <p><strong>Author:</strong> <a href="${item.user.html_url}" target="_blank">${item.user.login}</a></p>
                <p><strong>Status:</strong> ${item.state}</p>
                <p><strong>Created:</strong> ${new Date(item.created_at).toLocaleString()}</p>
                <p><strong>Last Updated:</strong> ${new Date(item.updated_at).toLocaleString()}</p>
                <h4>Body:</h4>
                <pre style="white-space: pre-wrap; background: #f4f4f4; padding: 10px; border-radius: 4px;">${item.body || 'No body content provided.'}</pre>
            `;
        } catch (error) {
            console.error('Error fetching item details:', error);
            itemDetailsDiv.innerHTML = `<p class="error">Failed to load details: ${error.message}</p>`;
        }
    };

    // Event listeners
    fetchButton.addEventListener('click', fetchData);
    itemSelector.addEventListener('change', showDetails);

    // Initial setup (optional: pre-fill a repo for testing)
    // repoUrlInput.value = 'https://github.com/facebook/react'; 
});

In this frontend setup, we have an input field for the repository URL, a dropdown to choose between PRs and Issues, and a button to trigger the data fetch. The script.js file contains the core logic. The isValidRepoUrl function checks if the entered URL matches the expected GitHub format, and getRepoPath extracts the user/repo string, which is essential for constructing our API calls. When the 'Fetch Data' button is clicked, the fetchData function is executed. It first validates the URL, then constructs the request to our Node.js proxy server using the selected repository and data type. The response from the proxy (a list of PRs or Issues) is then used to populate the second dropdown menu (itemSelector). If the user selects an item from this dropdown, the showDetails function is called. This function makes another request to our proxy to fetch the specific details of the selected PR or Issue, which are then displayed in the itemDetailsDiv. We've also included basic error handling to display messages to the user if anything goes wrong during the fetching process. This interactive frontend, powered by our Node.js proxy, provides a clean and efficient way to explore GitHub repository data.

Displaying PR/Issue Details

Once the user has selected a repository and chosen between PRs and Issues, and subsequently picked a specific item from the populated dropdown, the next crucial step is to display the details of that selected Pull Request or Issue. Our frontend script already has the showDetails function set up to handle this. This function retrieves the selectedItemId from the itemSelector dropdown and, based on the chosen data type and repository, constructs a specific API endpoint to fetch the detailed information. For instance, fetching the details of a Pull Request typically involves an endpoint like /repos/:owner/:repo/pulls/:pull_number. Similarly, for an Issue, it would be /repos/:owner/:repo/issues/:issue_number. Our Node.js proxy server is configured to handle these requests by forwarding them to the GitHub API. The showDetails function then takes the detailed JSON response from the proxy and formats it nicely for display in the itemDetailsDiv. This includes showing the Title, Author (with a link to their GitHub profile), Status (open/closed/merged), Creation Date, Last Updated Date, and the Body content of the PR or Issue. We're using toLocaleString() to display dates in a human-readable format and a <pre> tag with some basic styling for the body content to preserve formatting, especially for code snippets or markdown. This ensures that all the relevant information is presented clearly and effectively to the user. The showDetails function also includes its own error handling, so if there's an issue fetching the specific item's details, an error message will be displayed instead of blank content. This completes the core functionality of our interactive viewer, allowing users to seamlessly navigate through repository data. The ability to fetch and display these details interactively without manual page reloads or dealing with CORS makes this a powerful tool for developers and contributors alike. It streamlines the process of reviewing code, understanding bug reports, or simply keeping track of repository activity.

Enhancements and Optional Features

While we've built a fully functional interactive GitHub PR/Issue viewer, there's always room for improvement and adding more advanced features. These enhancements can significantly boost the utility and user experience of our tool. One of the most important considerations when interacting with any public API, including the GitHub API, is rate limiting. GitHub imposes limits on how many requests you can make within a certain time period. To prevent our application from hitting these limits, we can implement logic in our Node.js proxy server to track API usage and potentially add caching mechanisms. For instance, we could store responses for a short period, returning cached data for subsequent identical requests instead of hitting the GitHub API again. Another valuable feature could be the ability to select multiple PRs or Issues and generate a combined summary. Imagine selecting several bug reports and getting a consolidated list of their titles, assignees, and severity labels. This would be incredibly useful for project managers or team leads. To achieve this, the frontend would need a way to select multiple items (perhaps using checkboxes), and a new function would aggregate the selected data before displaying it. For agent testing or more advanced filtering, we could allow users to highlight PRs/Issues that meet certain conditions. This could involve checking for specific labels (like bug or enhancement), checking if a PR is mergeable, or if an issue is assigned to a particular user. This would require adding more sophisticated parsing of the GitHub API response and conditional rendering on the frontend. Implementing these features would involve extending both the Node.js proxy (to handle more complex requests or data processing) and the frontend JavaScript (to manage multi-select states, aggregation logic, and conditional styling). For example, to handle rate limiting more gracefully, the Node.js server could check the X-RateLimit-Remaining header from GitHub's response and inform the user or throttle requests proactively. For combined summaries, the frontend could fetch a list, allow multiple selections, and then re-fetch or display details from the initially fetched list, aggregated into a new view. The possibilities for customization are vast, allowing you to tailor the tool to specific workflows and needs. These optional features transform a simple viewer into a more powerful project management and development aid.

Conclusion

We've successfully built an interactive GitHub PR/Issue viewer using Node.js as a proxy to bypass CORS issues. This solution provides a seamless way to fetch and display repository data directly from the GitHub API, offering a much-needed alternative to manual browsing or complex setups. By leveraging Node.js and Express.js for the backend proxy and plain JavaScript for the frontend, we've created a dynamic and user-friendly tool. This approach not only solves common web development challenges like CORS but also demonstrates a practical application of server-side proxying for client-side applications. The ability to input a repository URL, select the data type, view a list of items, and inspect their detailed content makes this a highly effective tool for developers, project managers, and anyone who frequently interacts with GitHub repositories. Remember to always be mindful of API rate limits when making requests to services like GitHub. For more advanced use cases, consider implementing caching or more sophisticated error handling in your Node.js proxy. The flexibility of this setup means you can further customize and expand upon it, adding features like searching, filtering, or even integrating with other services. Experimenting with different UI elements and backend logic will undoubtedly lead to an even more powerful and personalized tool. Happy coding!

For further exploration into API best practices and advanced Node.js techniques, I recommend checking out the official documentation for Express.js and Axios. Understanding how to effectively manage HTTP requests and build robust web applications is key to developing sophisticated tools like this.