ScriptRunner Connect External Coding Series #3: API Magic

Skip to page content

Building Local Clients That Don't Make You Want to Throw Your Computer Out the Window

Ever tried to debug an API integration by squinting at console logs in a browser? Or worse – adding console.log statements and redeploying just to see what data you're getting? Those dark days are over, my friend.


Welcome to the third instalment of our ScriptRunner Connect External Coding series, where we're about to turn your local development environment into an API debugging paradise. If you've been following along, you've already set up your accessible WebStorm development environment with SFTP in ScriptRunner Connect External Coding Series #1: Your IDE, Your Rules - Setting Up Accessible Development, and mastered local testing with runtime mocks and Jest in ScriptRunner Connect External Coding Series #2: Testing Without Tears - Mocking APIs Like a Pro. Now it's time for the really exciting part – building local API clients that make real HTTP requests to Atlassian Cloud while maintaining the accessible development experience you deserve.

The Local API Development Revolution

Here's the thing about cloud development that nobody talks about: you shouldn't need an internet connection to understand your code. ScriptRunner Connect's External Coding lets you build local API clients that make real HTTP requests to Atlassian Cloud – all while maintaining the accessible development experience you've been building throughout this series.

Why This Changes Everything (Especially for Accessibility)

Building on the foundation from our previous posts, local API development provides:

Screen Reader Friendly Debugging: Your screen reader can properly navigate through:

Consistent Development Experience:

Integration with Your Testing Workflow:

The Architecture That Actually Makes Sense

ScriptRunner Connect provides a brilliant pattern for local API development. Instead of trying to mock everything (like we did for unit testing in Series #2: Testing Without Tears - Mocking APIs Like a Pro), you create local HTTP clients that extend the same managed API cores used in production. It's like having a direct phone line to Atlassian Cloud from your IDE.

The Magic Files

Building on the project structure you downloaded in Series #1: Your IDE, Your Rules - Setting Up Accessible Development, here's what makes local API development possible:

node/
├── client/jira/cloud/JiraCloudClient.ts    # Your local HTTP client    (new!)
├── apiRegistry.ts                          # Central API management    (new!)
├── external/coding/                        # Local execution scripts   (new!)
├── runtimeMocks.ts                         # Runtime mocks             (from Series #2)
└── tests/                                  # Jest tests                (from Series #2)

Each file has a specific job, and together they create an environment where you can develop and debug with real data while maintaining the testing capabilities you built in Series #2: Testing Without Tears - Mocking APIs Like a Pro.

Building Your First Local API Client

Let's create a Jira Cloud client that you can actually debug and understand. This builds on the development environment you set up in Series #1: Your IDE, Your Rules - Setting Up Accessible Development and complements the testing framework from Series #2: Testing Without Tears - Mocking APIs Like a Pro.

The Foundation - JiraCloudClient.ts

 
// node/client/jira/cloud/JiraCloudClient.ts
import {
    PlatformImplementation,
    Request,
    Response as CommonsResponse,
    Headers as CommonsHeaders,
    BaseApiCore,
} from '@managed-api/commons-core';
import { JiraCloudApiCore } from '@managed-api/jira-cloud-v3-core';
 
/**
 * Local implementation of Jira Cloud API client for development and testing.
 *
 * Extends JiraCloudApiCore to provide HTTP-based communication with Jira Cloud
 * using Basic Authentication. This client enables local development by making
 * direct API calls to Atlassian Cloud services.
 *
 * @example
 * ```typescript
 * const client = new JiraCloudClient(
 *   'your-email@company.com',
 *   'your-api-token'
 * );
 * ```
 */
export class JiraCloudClient extends JiraCloudApiCore {
    constructor(
        private baseUrl: string,
        private email: string,
        private apiToken: string,
    ) {
        super();
    }
 
    protected getPlatformImplementation(): PlatformImplementation {
        return {
            buffer: {
                encode: (input) => Buffer.from(input, 'utf-8'),
                decode: (input) => Buffer.from(input).toString('utf-8'),
            },
            performHttpCall: (request) => this.performHttpCall(request),
        };
    }
 
    private async performHttpCall(request: Request): Promise<CommonsResponse> {
        const requestUrl = `${this.baseUrl}${request.url}`; // Substitute base URL
        request.headers.set('Authorization', `Basic ${btoa(`${this.email}:${this.apiToken}`)}`); // Substitute auth token
 
        const requestHeaders = new Headers();
        request.headers.forEach((value, key) => requestHeaders.append(key, value));
 
        const response = await fetch(requestUrl, {
            method: request.method,
            headers: requestHeaders,
            body:
                request.method.toLowerCase() !== 'get'
                    ? Buffer.from((await request.arrayBuffer()) ?? new ArrayBuffer(0))
                    : undefined,
        });
 
        const responseHeaders = new CommonsHeaders();
        response.headers.forEach((value, key) => responseHeaders.append(key, value));
 
        const body = await response.arrayBuffer();
 
        return super.buildResponse(response.url, response.status, response.statusText, responseHeaders, body);
    }
}

The Registry - apiRegistry.ts

We are going to comment references to the mock JiraCloudClientMock.

 
// node/apiRegistry.ts
import { config } from 'dotenv';
import { BaseApiCore } from '@managed-api/commons-core';
import { JiraCloudClient } from "./client/jira/cloud/JiraCloudClient";
//import { JiraCloudClientMock } from './client/jira/cloud/JiraCloudClientMock';
 
// Load environment variables from .env file
config();
 
export type ApiRegistry = Record<string, BaseApiCore>;
 
export const API_REGISTRY: ApiRegistry = {
    // Declare here locally implemented API connections
 
    // Client without making real HTTP requests
    //'jira/cloud': new JiraCloudClientMock(),
 
    // Client making real HTTP requests
    'jira/cloud': new JiraCloudClient(
        'your-email@company.com',
        'your-api-token'
    ),
};
 

Security is important, but it shouldn't be complicated. You should consider extracting parameters such as baseURL, email and api-token to Environment Variables.

Creating API Integration Scripts

Now let's build a script that uses your local API client. This is where the magic happens – you're making real API calls but with full local debugging capabilities, all within the accessible WebStorm environment you configured in Series #1: Your IDE, Your Rules - Setting Up Accessible Development.

The Projects Integration

 

 
// scripts/cloud/atlassian/jira/Projects.ts
import { GetProjectsResponseOK } from '@managed-api/jira-cloud-v3-core/types/project';
import JiraCloud from '../../../api/jira/cloud';
 
/**
 * Recursively fetches all projects from Jira Cloud with pagination support.
 *
 * @param pageNumber - The page number to fetch (starts from 1)
 * @param projects - Accumulator array for collected projects across all pages
 * @returns Promise that resolves to an array of all projects
 */
export async function getProjects(pageNumber = 1, projects: GetProjectsResponseOK['values'] = []): Promise<GetProjectsResponseOK['values']> {
    try {
        const response = await JiraCloud.Project.getProjects()
 
        if(response.values) {
            console.log(`Found ${response.values.length} projects on page ${pageNumber}`);
            projects.push(...response.values)
        }
 
        if(!response.isLast) {
            console.log(`More pages available, fetching page ${pageNumber + 1}...`);
            return await getProjects(pageNumber + 1, projects)
        }
 
        console.log(`Total projects retrieved: ${projects.length}`);
        return projects
 
    } catch (error) {
        console.error(`Error fetching projects on page #${pageNumber}:`, error.message);
        throw(error);
    }
}

Local Execution Magic

The real superpower is being able to run and debug your scripts locally with real data, using the same accessible terminal environment you've been developing throughout this series:

Local Execution Script

 

 
// node/external/coding/OnJiraCloudProjectCreated.ts
import OnJiraCloudProjectCreated from "../../../scripts/OnJiraCloudProjectCreated";
import { ProjectCreatedEvent } from "@sr-connect/jira-cloud/types/project";
 
/**
 * Local execution script for testing project creation events.
 */
(async () => {
    await OnJiraCloudProjectCreated({} as ProjectCreatedEvent, {
        availableMemory: 400,
        invocationId: 'INVOCATION_ID',
        triggerType: 'MANUAL',
        rootTriggerType: 'MANUAL',
        startTime: Date.now(),
        timeout: 900000,
        environment: {
            uid: 'ENVIRONMENT_UID',
            name: 'Default',
            vars: {
                // Environment variables defined in ../../../ev.ts
            },
        }
    })
})().catch(console.error);

Advanced Testing with Real APIs

Now comes the really exciting part – combining the testing framework from Series #2: Testing Without Tears - Mocking APIs Like a Pro with real API data while maintaining accessibility:

Integration Testing That Actually Works

 

 
// node/tests/cloud/atlassian/jira/Projects.test.ts
import '../../../../runtimeMocks';
import { jest } from '@jest/globals';
import { getProjects } from "../../../../../scripts/cloud/atlassian/jira/Projects";
 
// Array for intercepted console.log messages
let interceptedConsoleLogs: string[] = [];
 
// Function for getting mocked Context object
export function getContext(ev: EV): Context<EV> {
    return {
        availableMemory: 400,
        invocationId: 'INVOCATION_ID',
        triggerType: 'MANUAL',
        rootTriggerType: 'MANUAL',
        startTime: Date.now(),
        timeout: 900000,
        environment: {
            uid: 'ENVIRONMENT_UID',
            name: 'Default',
            vars: ev,
        },
    };
}
 
beforeAll(() => {
    // Mock all console.log methods to intercept messages and store them
    jest.spyOn(console, 'log').mockImplementation((...args) => interceptedConsoleLogs.push(args.join(' ')));
});
 
// npm run test
describe('Projects API Integration Tests', () => {
 
    test('Should fetch real projects from Jira Cloud', async () => {
        const projects = await getProjects();
 
        // Test with real data
        expect(projects).toBeDefined();
        expect(Array.isArray(projects)).toBe(true);
        expect(projects.length).toBeGreaterThan(0);
 
        // Test project structure
        if (projects.length > 0) {
            const firstProject = projects[0];
            expect(firstProject).toHaveProperty('id');
            expect(firstProject).toHaveProperty('key');
            expect(firstProject).toHaveProperty('name');
        }
    });
});
 
afterAll(() => {
    jest.clearAllMocks();
});

The Accessibility-First Development Workflow

Here's a development workflow that leverages your accessible local environment, building on everything you've set up in this series:

Develop with Real Data

Install `tsx` globally to execute TypeScript files directly:

 
# Install tsx globally for direct TypeScript execution
npm install -g tsx
 
# Run your script locally with real API calls
tsx node/external/coding/OnJiraCloudProjectCreated.tx

 

Deploy via SFTP

Use the SFTP configuration from Series #1: Your IDE, Your Rules - Setting Up Accessible Development to deploy your tested scripts:

  • Upload via WebStorm's deployment tools
  • Monitor upload progress in accessible Event Log
  • Verify deployment in ScriptRunner Connect's Scripts section

 

What We've Accomplished

API mastery unlocked! You've just graduated from "guessing what the API returns" to "debugging with real data like a wizard." Your local HTTP clients are chatting with Atlassian Cloud like old friends, your screen reader is announcing successful API calls with satisfaction, and you're debugging with the confidence of someone who actually knows what's happening.

Congratulations, you've essentially built a secure tunnel between your IDE and Atlassian's servers. It's like having a direct hotline to Jira and Confluence, except instead of calling to complain about bugs, you're calling to understand them, fix them, and maybe even prevent them. Your future self will thank you when 3 AM debugging sessions become pleasant afternoon code reviews.

The grand finale awaits: ScriptRunner Connect External Coding Series #4: AI-Powered Development - Your New Coding Superpower (That Actually Understands Accessibility) is where we bring in AI assistance that's so well-trained on accessibility, it probably writes better JSDoc comments than most humans. Get ready for code generation that actually makes sense when your screen reader announces it.

Ready to supercharge your development with AI assistance? Check out our final post in this series.

Previous posts in this series:

About the Author and Development Team

IMG_7964 (1)Rafael Pinto Sperafico, Senior Atlassian Consultant and technical architect behind the ScriptRunner Connect External Coding Series, brings years of hands-on experience helping organisations transform their Atlassian automation development workflows. As part of Ambientia's expert Atlassian team, Rafael Pinto Sperafico specialises in accessibility-first development practices, enterprise-grade ScriptRunner Connect implementations, and cutting-edge AI-assisted development methodologies. This series emerged from real-world client work where teams needed professional, accessible development environments that didn't force them to abandon their familiar tools or compromise on functionality.

The comprehensive insights shared throughout this series reflect the collective expertise of Ambientia's Atlassian Consultants Team – experienced professionals passionate about making powerful development tools accessible to all developers. Our team combines technical expertise in ScriptRunner Connect with a deep understanding of accessibility requirements, helping development teams explore professional local development environments that integrate with cloud execution. We specialise in the methodologies covered in this series: WebStorm SFTP configuration, comprehensive testing frameworks, local API development, and AI-powered automation workflows that respect accessibility standards.

Ready to transform your ScriptRunner Connect development experience? Whether you're struggling with browser-based development limitations, need to implement accessibility-compliant automation workflows, or want to leverage AI assistance like Claude Code while maintaining professional development standards, Ambientia's Atlassian experts are here to help. We offer tailored consultancy for External Coding implementations, accessibility audits for existing automations, and comprehensive training on the workflows covered in this series. Contact Ambientia today to discuss how our proven External Coding methodologies can accelerate your team's productivity while ensuring every developer – regardless of their assistive technology needs – can contribute effectively to your Atlassian automation projects.

More related