Course menuForge Kitchen

Level 1 · Lesson 4

Call the supplier

Write a small module that asks API Ninjas for a recipe, have the resolver use it, let the card ask for it, and put the supplier on the app’s approved list.

About 35 min7 steps

Behind, or starting here? Download the code as it should be before this lesson.

This is the round trip the whole course is built on. The card asks for a dish, the cook in the cloud takes the key from the safe and buys the recipe from the supplier, and the card shows it. Play it once before you start; each step below builds one movement.

One click, one recipe

The card calls invoke('getDish').

In the kitchen: The card sends the runner to the cook.

Runs in the browser · Front of house

Issue panel The recipe card

A module that adds your UI to the work item view, below the description.

A card clipped to every dish ticket, filled in by your kitchen.

Select any box in the diagram to see what it is.

For now the cook always asks for Pad thai. Lesson 6 makes it read the dish from the ticket.

Step 1: Add Forge’s API package

Functions reach the outside world through @forge/api, Forge’s own package for fetch and the product APIs. Install it in the app folder:

RunTerminal
npm install @forge/api

npm writes this line into package.json for you:

Editpackage.json

npm install @forge/api writes this line for you.

"license": "MIT",
"private": true,
"dependencies": {
Added: "@forge/api": "^8.2.0",
"@forge/bridge": "^7.1.0",
"@forge/react": "^12.3.0",
"@forge/resolver": "^2.0.0",

Why: External API

Stuck? “Cannot find module @forge/…” · I added an npm package

Step 2: Write the supplier module

Outside Atlassian · Suppliers

External API · a supplier

An external API is any service outside Atlassian that your functions call over HTTPS. Here it is API Ninjas: you send a dish name and your key in the X-Api-Key header, and it answers with a list of matching recipes as JSON. Only functions call it, never the browser, so the key never leaves the cloud.

In the kitchen The supplier sends what your kitchen can’t make itself, such as a recipe for the dish on the ticket.

Create the folder src/supplier/ and a new file in it. It knows how to talk to API Ninjas and nothing else:

New filesrc/supplier/apiNinjas.js

A new file that knows how to talk to API Ninjas.

Added: import { fetch } from '@forge/api';
Added:
Added: // The supplier: API Ninjas' recipe API. The caller passes the key in after reading
Added: // it from an encrypted variable, so it never appears in code or in the browser.
Added: // unverified: response fields (title, ingredients separated by "|", servings,
Added: // instructions) and free-tier limits, from API Ninjas' public docs.
Added: const RECIPE_URL = 'https://api.api-ninjas.com/v1/recipe';
Added:
Added: export async function findDish(query, apiKey) {
Added: if (!apiKey) {
Added: throw new Error('No API Ninjas key. Set it with forge variables set --encrypt, then deploy again.');
Added: }
Added: const params = new URLSearchParams({ query });
Added: const response = await fetch(`${RECIPE_URL}?${params}`, {
Added: headers: { 'X-Api-Key': apiKey },
Added: });
Added: if (response.status === 429) {
Added: throw new Error('The supplier is busy (rate limit). Try again in a minute.');
Added: }
Added: if (!response.ok) {
Added: throw new Error(`The supplier answered ${response.status}.`);
Added: }
Added: const recipes = await response.json();
Added: if (!Array.isArray(recipes) || recipes.length === 0) return null;
Added: return toDish(recipes[0]);
Added: }
Added:
Added: // Turn the supplier's format into the one the card uses.
Added: function toDish(recipe) {
Added: return {
Added: title: String(recipe.title ?? ''),
Added: servings: String(recipe.servings ?? ''),
Added: ingredients: String(recipe.ingredients ?? '')
Added: .split('|')
Added: .map((item) => item.trim())
Added: .filter(Boolean),
Added: method: String(recipe.instructions ?? ''),
Added: };
Added: }

Why: External API · Environment variable

Three things to notice:

  • findDish does not read the key itself. Whoever calls it passes the key in, so the module never knows where secrets live.
  • The answer is always a list, even with one match. recipes[0] is the best match, and an empty list means the supplier has no such dish.
  • toDish turns the supplier’s format into the card’s own: a title, servings, a list of ingredients and a method. If you ever switch suppliers, this file is the only one that changes. Level 2 reuses it as it is.

Stuck? I changed code in src/

Step 3: Teach the cook a new dish

The template’s resolver answers getText. Replace it with getDish, which takes the key from the safe and asks the supplier:

Editsrc/resolvers/index.js
  • Bring in the supplier module.
  • Replace getText with getDish, which asks the supplier.
import Resolver from '@forge/resolver';
Added: import { findDish } from '../supplier/apiNinjas';
const resolver = new Resolver();
Removed: resolver.define('getText', (req) => {
Removed: console.log(req);
Removed: return 'Hello, world!';
Added: resolver.define('getDish', async () => {
Added: const query = 'pad thai';
Added: const dish = await findDish(query, process.env.API_NINJAS_KEY);
Added: return { query, dish };
});
export const handler = resolver.getDefinitions();

Why: Resolver · Environment variable

process.env.API_NINJAS_KEY is the variable from lesson 3. The resolver returns both the query and the dish, so the card can say what it looked for when the supplier finds nothing.

Stuck? The card says “No API Ninjas key”

Step 4: Send the runner

The card asks for getDish when it opens and fills in the sample layout with the answer:

Editsrc/frontend/index.jsx
  • Bring back the hooks and invoke.
  • Ask for getDish when the card opens.
  • Show what came back.
Removed: import React from 'react';
Added: import React, { useEffect, useState } from 'react';
import ForgeReconciler, { Heading, Inline, Lozenge, Stack, Text } from '@forge/react';
Added: import { invoke } from '@forge/bridge';
const App = () => {
Added: const [result, setResult] = useState(null);
Added: useEffect(() => {
Added: invoke('getDish').then(setResult);
Added: }, []);
Added: const dish = result?.dish;
return (
<Stack space="space.200">
<Inline space="space.100" alignBlock="center">
Removed: <Heading size="medium">Pad thai</Heading>
Removed: <Lozenge>2 servings</Lozenge>
Added: <Heading size="medium">{dish ? dish.title : 'Loading...'}</Heading>
Added: {dish && <Lozenge>{dish.servings}</Lozenge>}
</Inline>
Removed: <Text>The supplier’s recipe goes here.</Text>
Added: <Text>{dish ? dish.method : ''}</Text>
</Stack>
);
};

Why: invoke()

Until the answer arrives, dish is empty, so the heading says Loading… for a moment. Lesson 5 replaces that with a proper waiting state.

Step 5: Put the supplier on the approved list

Declared in manifest.yml · The licence

Egress permission · the approved supplier list

Forge blocks every call from a function to an outside host unless the manifest lists it. Backend calls go under permissions.external.fetch.backend. List the host name only, without https:// or a path. Each host you add is shown to the admin who installs the app.

In the kitchen The back door only opens for suppliers on the approved list.

Atlassian docs: Runtime egress permissions (opens in a new tab)

It fetches the recipe from API Ninjas.It calls the approved supplier.

Without this, the code is right and the call still fails. Add the host at the end of manifest.yml:

Editmanifest.yml

Allow calls to API Ninjas from your functions.

memoryMB: 256
architecture: arm64
id: ari:cloud:ecosystem::app/<your-app-id>
Added: permissions:
Added: external:
Added: fetch:
Added: backend:
Added: - address: api.api-ninjas.com

Why: Egress permission · Major version

Stuck? The logs show a fetch to api.api-ninjas.com was blocked

Step 6: Deploy and sign the new licence

Declared in manifest.yml · The licence

Major version · a new licence

A deploy that asks for more than the last version could, such as a new outside host or a new scope, becomes a new major version. The CLI stops and asks you to approve it, so nobody widens an app’s permissions by accident. Code-only changes stay minor and reach sites by themselves.

In the kitchen Asking for more rights means a new licence, which the landlord has to sign.

Atlassian docs: Environments and versions (opens in a new tab)

Lives on your laptop · Home kitchen

forge install --upgrade · the landlord signs the new licence

forge install --upgrade moves an existing installation onto the newest major version, so the site grants the new permissions. Until you run it, the site keeps running the old version with the old permissions, even though the deploy succeeded.

In the kitchen Until the landlord signs, the location keeps working under the old licence.

Atlassian docs: forge install (opens in a new tab)

Stop the tunnel if it is running, then deploy:

RunTerminal
forge deploy

It stops on purpose and names the rule to approve. Approve it, then upgrade the installation on your site:

RunTerminal
forge deploy --approve MAJOR_VERSION_RULE
forge install --upgrade

Pick the installation on your developer site and confirm.

You should seeExampleYour names, keys and ids will differ.
$ forge install --upgrade
? Select the site or workspace to upgrade: your-name.atlassian.net (Jira)
✔ Upgrade complete!

Stuck? Deploy stops after a permission change · The tunnel is running while I deploy or upgrade · `forge install` warns about installing a development app on a production site

Step 7: Open the Pad thai ticket

Reload the Pad thai ticket. After a moment the card shows the supplier’s title for the dish, its servings, and the method.

You should see, on the work itemExampleAn example: the supplier’s wording and servings will differ.

Recipe card

Pad Thai 4 servings

Soak the noodles in warm water, then stir-fry with the sauce, egg and bean sprouts…

If the heading stays on Loading…, the call failed. forge logs shows why; lesson 5 makes the card say it too.

Stuck? The card says “No API Ninjas key” · The card says “The supplier answered 401” (or 403) · The card says “The supplier is busy” · The logs show a fetch to api.api-ninjas.com was blocked

Compare with yours: download the code after this lesson.

Type at least two letters.