npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@valiantys/atlassian-app-backend

v1.0.0

Published

This library provides utility functions to handle all the setup necessary to support a Forge app backend that can run deployed or in standalone mode

Downloads

74

Readme

@valiantys/atlassian-app-backend

This library provides utility functions to handle all the setup necessary to support a Forge app backend that can run deployed or in standalone mode.

Note: The library currently only supports resolver functions and Forge Storage in standalone mode (without a tunnel).

Using the library

Creating a new App

Use the Forge CLI to create a new Forge App with a Custom UI

npm install -g @forge/cli@latest
forge create -d forge-app -t jira-global-page-custom-ui

Add Typescript support for the backend

Install the necessary libraries and initialize typescript compiler config.

npm i --save-dev typescript nodemon
npm i dotenv
npx tsc --init

Add "exclude" and "outDir" to tsconfig.json:

{
  "exclude": ["node_modules", "dist", "static", "**/*spec.ts"],
  "compilerOptions": {
    "outDir": "dist",
    ...
  }
}

Add scripts to package.json.

{
  "scripts": {
    "build": "tsc",
    "dev": "tsc -w & nodemon -q -w dist dist/index.js",
    "start": "tsc && node ./dist/index.js"
  }
}

Install backend library

npm i @valiantys/atlassian-app-backend

Add an environment file

Add a .env file in the root of the project, and add .env to your project's .gitignore file. You should also add a sample.env file to your project that does NOT contain any secrets but makes it easier for other developers to set up their own .env file.

IS_STANDALONE=true

# Needed for standalone authentication with Atlassian OAuth
ATLASSIAN_OAUTH_CLIENT_ID=<client-id>
ATLASSIAN_OAUTH_CLIENT_SECRET=<client-secret>

# Needed for mock forge storage implementation
FORGE_STORAGE_FILE_PATH=
FORGE_STORAGE_INDEX_FIELD_MAPPING=

See https://developer.atlassian.com/cloud/jira/software/oauth-2-3lo-apps/#enabling-oauth-2-0--3lo- for instructions on creating an OAuth App to use for authentication. The callback url will be http://localhost:4200/callback if your UI module is running locally on port 4200.

Note: If you will be running more than one frontend app locally at the same time, you will need to configure a different OAuth App for each frontend port number (4200, 4201, etc.), and make sure they consistently choose the same port.

index.ts

Rename src/index.js to index.ts and replace contents with:

import { bootstrapExpress, expressDefaults, registerFunctions } from '@valiantys/atlassian-app-backend';

import Resolver from '@forge/resolver';
import * as dotenv from 'dotenv';

import { getForgeFunctions } from './lib/forge-functions';
import { getStandaloneFunctions } from './lib/standalone-functions';

dotenv.config();

const IS_STANDALONE = process.env.IS_STANDALONE === 'true';

if (IS_STANDALONE) {
  const port = parseInt(process.env.PORT || '3000');

  // OAuth settings
  const clientId = process.env.ATLASSIAN_OAUTH_CLIENT_ID || '';
  const clientSecret = process.env.ATLASSIAN_OAUTH_CLIENT_SECRET || '';

  // Mock Forge storage settings
  const filePath = process.env.FORGE_STORAGE_FILE_PATH || '';
  const indexFieldMapping = JSON.parse(process.env.FORGE_STORAGE_INDEX_FIELD_MAPPING || '{}');

  bootstrapExpress(getStandaloneFunctions(), 'jira', { clientId, clientSecret }, { ...expressDefaults, port }, { indexFieldMapping, filePath });
}

let resolver: Resolver | null = null;
if (!IS_STANDALONE) {
  resolver = new Resolver();
  registerFunctions(resolver, getForgeFunctions());
}
export const handler = resolver?.getDefinitions();

Example Resolver Function definition files

By abstracting out Forge dependencies into services, we are able to run the same functions in both the deployed Forge environment and in a standalone mode. In the example below, you will see that these functions are defined in the handler-functions.ts file, and depend on environment-specific services that are passed in. These services are initialized when the index file logic calls either the getForgeFunctions() or the getStandaloneFunctions() method based on the IS_STANDALONE environment variable.

docs/backend-files.png

forge-functions.ts
import { storage, startsWith, WhereConditions } from '@forge/api';
import { createServerSideForgeProductFetchService, ResolverFunctionMap } from '@valiantys/atlassian-app-backend';

import { handlerFunctions } from './handler-functions';

export function getForgeFunctions(): ResolverFunctionMap {
  const productFetchService = createServerSideForgeProductFetchService('user', 'jira');
  const forgeStorageService = {
    storage,
    WhereConditions,
    startsWith,
  };

  // Here you could initialize any other deployed-specific services or config
  // to pass to handlerFunctions as needed.
  return handlerFunctions(productFetchService, forgeStorageService);
}
standalone-functions.ts

The getStandaloneFunctions() function actually returns a function that is called by the library's express.js application setup. The library code will handle the details of passing in an implementation of a fetch service and the forge storage service. If you are targeting Confluence or Bitbucket instead of Jira, simply change the type parameter you see below.

import { AtlassianProductFetchService, ForgeStorageServiceStandalone, ResolverFunctionFactory } from '@valiantys/atlassian-app-backend';
import { handlerFunctions } from './handler-functions';

export function getStandaloneFunctions(): ResolverFunctionFactory<'jira'> {
  return (productFetchService: AtlassianProductFetchService<'jira'>, forgeStorageService: ForgeStorageServiceStandalone) => {
    // Here you could initialize any other standalone-specific services or config
    // to pass to handlerFunctions as needed.
    return handlerFunctions(productFetchService, forgeStorageService);
  };
}
handler-functions.ts

This file contains generic function definitions that can be used in either environment. All environment-specific configuration or implementation should be passed in as parameters.

import { AtlassianProductFetchService, ForgeRequest, ForgeStorageService } from '@valiantys/atlassian-app-backend';

// Add any additional environment specific config/services as parameters
// to handlerFunctions()
export function handlerFunctions(productFetchService: AtlassianProductFetchService<'jira'>, forgeStorageService: ForgeStorageService) {
  return {
    getText: {
      function: () => {
        return { text: 'Hello world!' };
      },
    },

    /*
      Making an Atlassian product API request (@forge/api requestJira)
     */
    getIssueTypes: {
      function: () =>
        productFetchService.fetch<IssueTypeDetails[]>({
          url: productFetchService.route`/rest/api/2/issuetype`,
          method: 'GET',
        }),
    },

    /**
     * Forge storage examples
     */
    getMyName: {
      function: async () => {
        const name = await forgeStorageService.storage.get('myName');
        return { name };
      },
    },
    setMyName: {
      function: async ({ payload }: ForgeRequest<{ name: string }>) => {
        await forgeStorageService.storage.set('myName', payload.name);
        const name = await forgeStorageService.storage.get('myName');
        return { name };
      },
      updatesForgeStorage: true, // triggers save of local file when in standalone mode
    },

    /**
     * This example demonstrates how one can access the user's account ID
     * in either deployed or standalone mode.
     * This context is made available on every backend request.
     */
    whoAmI: {
      function: async ({ context }: ForgeRequest<void>) => {
        console.log(context.accountId);
        return { myId: context.accountId };
      },
    },
  };
}

interface IssueTypeDetails {
  avatarId: number;
  description: string;
  hierarchyLevel: number;
  iconUrl: string;
  id: string;
  name: string;
  self: string;
  subtask: boolean;
}

Add a UI module

See the README file in the @valiantys/atlassian-app-frontend library for instructions on adding a Custom UI module to your app.

Run the app locally (standalone mode)

npm run dev

Forge Storage in standalone mode

When running in standalone mode, the app will be using an in-memory implementation of the Forge storage API, which persists changes to a local json file. When the app starts up it will check for an existing json file and load it into memory. Every time a resolver function completes, the json file will be updated if the function was marked with updatesForgeStorage:true. The location of the storage.json file is controlled by the environment variable FORGE_STORAGE_FILE_PATH, which should be set in your .env file.

Deploy the app

forge deploy

Install

forge install --site <your-site-name>.atlassian.net --product=jira --no-interactive --environment <env-name>

Configure CORS

By default, the standalone backend allows cross-origin requests from http://localhost:4200 and http://localhost:4201. To change this configuration, you may pass in a custom origin configuration to the bootstrapExpress function in index.ts.

Example:

bootstrapExpress(getStandaloneFunctions(), 'jira', { clientId, clientSecret }, { ...expressDefaults, port, corsOptions: { origin: 'http://example.com' } }, { indexFieldMapping, filePath });

You can pass in any CORS options as defined here: https://www.npmjs.com/package/cors#configuration-options

Unit testing

Install jest

npm i -D jest ts-jest @types/jest

Configure Jest

Add a jest.config.js file in the project root directory.

/** @type {import('ts-jest').JestConfigWithTsJest} **/
module.exports = {
  modulePathIgnorePatterns: ['<rootDir>/static/'],
  testEnvironment: 'node',
  transform: {
    '^.+.ts?$': ['ts-jest', {}],
  },
};

Exclude this file in eslint.config.mjs.

{ignores: ["static/*", "dist/*", "jest.config.js"]},

Example Test

import { handlerFunctions } from './handler-functions';
import { atlassianOAuthJiraFetch, AtlassianProductFetchService, ForgeStorageServiceStandalone, ForgeStorageStandaloneData } from '@valiantys/atlassian-app-backend';

describe('handlerFunctions', () => {
  let storage: ForgeStorageStandaloneData;
  let forgeStorageService: ForgeStorageServiceStandalone;
  let productFetchService: AtlassianProductFetchService<'jira'>;

  beforeEach(() => {
    // use in-memory forge service for testing
    storage = {
      secrets: {},
      keyValues: {},
      entityCollections: {},
    };
    forgeStorageService = new ForgeStorageServiceStandalone({}, storage);

    // Mock out fetch for testing
    productFetchService = atlassianOAuthJiraFetch('test-token', 'test-site');
    productFetchService.fetch = jest.fn();
  });

  it('getText should return a text value', () => {
    const response = handlerFunctions(productFetchService, forgeStorageService).getText.function();
    expect(response).toEqual({
      text: 'Hello world!',
    });
  });
});

Example Application

You will find an examples directory under node_modules/@valiantys/atlassian-app-backend/examples, containing all the examples shown in this README along with more supporting files. The repo for the full example application is also available at https://bitbucket.org/oasisdigital/atlassian-app-examples.

If you would like to clone a template repo to get started more quickly, you can find one at https://bitbucket.org/oasisdigital/atlassian-app-template.

Support

Our issue-tracking board is viewable at https://trello.com/b/aRmPXQjq/atlassian-app-frontend-backend-npm-package-issue-tracker. To file an issue you may send it in an email to [email protected].

You may also contact us with questions at [email protected].