Skip to main content
Building a managed API (From REST consumer to API provider 3/3)
Share on socials
Script fragments are fed into a ScriptRunner Connect funnel and a fully formed script is built underneath

Building a managed API (From REST consumer to API provider 3/3)

Welcome to the From REST consumer to API provider guest tutorial. In this three-part series, Rafael Pinto Sperafico from The Adaptavist Group partner, Ambientia, will guide you through the world of REST APIs. First we got you started with the fundamentals in Getting to grips with the basics of REST APIs. Then in tutorial two, REST APIs made easy with ScriptRunner Connect, we created scripts for basic REST API operations, including GET, POST, PUT, PATCH, and DELETE requests, using the ReqRes API.
Now, in the final post of the series, we take things further by transforming our consumer-oriented scripts into a reusable, managed API. We'll build a TypeScript-based package that encapsulates the same operations, making the logic portable and maintainable. Additionally, we’ll demonstrate how to publish the managed API to npm and integrate it into ScriptRunner Connect.
What you will learn:
1. How to configure TypeScript with tsconfig.json
2. How to define types in ReqRes.js
3. How to create ReqResAPI.js
4. How to publish a managed API to npm
5. How to import into ScriptRunner Connect

Setting up the TypeScript project

To get started, let’s create a directory on your local environment so we can build our TypeScript-based package.
Create a new project folder:
mkdir reqres-api
cd reqres-api
npm init -y
Update package.json with the code below.
Important: This tutorial uses the package name "reqres-api". When following this tutorial, please replace this with a unique package name and note it down for use later.

package.json

{
    "name": "reqres-api",
    "version": "1.0.0",
    "description": "A managed API client for ReqResIn.",
    "main": "dist/index.js",
    "types": "dist/index.d.ts",
    "type": "module",
    "scripts": {
        "build": "tsc"
    },
    "keywords": ["api", "reqres", "typescript"],
    "author": "Rafael Pinto Sperafico",
    "license": "MIT",
    "devDependencies": {
        "typescript": "^5.0.2"
    },
    "files": ["dist", "README.md"],
    "publishConfig": {
        "access": "public"
    },
    "dependencies": {
        "@managed-api/generic-core": "^1.2.0"
    }
}
Explanation:
  • main: Specifies the entry point for the compiled JavaScript code.
  • types: Points to the compiled TypeScript declarations for type safety in consuming projects.
  • scripts: Build runs the TypeScript compiler.
  • files: Specifies which files to include in the published npm package.
  • publishConfig: Ensures the package is public by default.

Configure TypeScript with tsconfig.json

Create tsconfig.json in the project root:

tsconfig.json

{
    "compilerOptions": {
        "target": "ES2019",
        "module": "ES6",
        "outDir": "dist",
        "strict": true,
        "declaration": true,
        "esModuleInterop": true,
        "moduleResolution": "node"
    },
    "include": ["src/**/*"]
}
Explanation:
  • target: Outputs code compatible with ES2019.
  • module: Uses the ES6 module system.
  • outDir: Outputs compiled files to the dist directory.
  • strict: Enables strict type checking.
  • declaration: Generates .d.ts files for type definitions.
  • esModuleInterop: Ensures compatibility with CommonJS modules.
  • include: Includes all TypeScript files in the src directory.

Project structure

Create the following structure:
./src/index.ts
./src/ReqResAPI.ts
./src/ReqResType.ts

Define types in ReqResType.js

In this step, you define TypeScript interfaces that describe the structure of the data exchanged with the API. These include:
  • User: Represents a user object with fields like id, email, first_name, etc.
  • PaginatedUsersResponse: Models paginated responses, including metadata like page and the list of users (data).
  • CreateUserRequest/Response: Structures the payload for creating users and the server's response, including generated fields like id and timestamps.
  • UpdateUserResponse: Similar to CreateUserResponse, but used for updates.
Defining types ensures type safety, reducing errors and improving code clarity.

src/ReqResType.ts

/**
* Represents a user object
*/
export interface User {
	id: number;
	email: string;
	first_name: string;
	last_name: string;
	avatar: string;
}

/**
* Represents paginated responses for fetching users
*/
export interface PaginatedUsersResponse {
	page: number;
	per_page: number;
	total: number;
	total_pages: number;
	data: User[];
}

/**
* Defines the structure of a user creation request
*/
export interface CreateUserRequest {
	name: string;
	job: string;
}

/**
* Combines request fields with server-generated fields
*/
export interface CreateUserResponse extends CreateUserRequest {
	id: string;
	createdAt: string;
}

/**
* Similar to 'CreateUserResponse' but for updates
*/
export interface UpdateUserResponse extends CreateUserRequest {
	updatedAt: string;
}

Create ReqResAPI.ts

By encapsulating the operations into a reusable TypeScript package, we improve maintainability and create a scalable foundation for future integrations. This managed API can be easily imported and used in any ScriptRunner Connect workspace, unlocking the full potential of API automation.

Define the class and constructor

src/ReqResAPI.ts

import {
	PaginatedUsersResponse,
	CreateUserRequest,
	CreateUserResponse,
	UpdateUserResponse
} from './ReqResType';

//import { GenericAppApiCore } from "@managed-api/generic-core";
/**
* ReqResAPI provides a wrapper around the ReqRes API client to perform user operations.
* It includes methods for fetching, creating, updating, and deleting users.
*/
export class ReqResAPI {
	private client: any;
	// private client: GenericAppApiCore;

	/**
	* Creates an instance of ReqResAPI.
	* @param client - An instance of the Generic Connector created in ScriptRunner Connect.
	*/
	constructor(client: any) {
		this.client = client;
	}

	// Handle Requests
	// Implement API Methods
}

Handle requests

The handleRequest method centralises the logic for executing API requests and handling responses.
  • Purpose: Simplifies error handling and data parsing by wrapping request logic in a single method.
  • Error management: Returns a consistent structure with data or error fields, ensuring robust and predictable handling of API responses.

src/ReqResAPI.ts

// Handle Requests
/**
* Handles the execution of an API request.
* @param request - A function that returns a `Response` object when invoked.
* @returns A promise resolving to an object containing the parsed data or an error message.
*/
private async handleRequest<T>(request: () => Promise<Response>): Promise<{ data?: T; error?: string }> {
	try {
		const response = await request();
		if (!response.ok) {
			const errorText = await response.text();
			return { error: `Unexpected response code: ${response.status} - ${errorText}` };
		}
		const data = await response.json();
		return { data };
	} catch (err) {
		return { error: `Network or parsing error: ${(err as Error).message}` };
	}
}

Implement API methods

This involves adding specific methods in the ReqResAPI class for each API operation:
  • getUsers: Fetches users with pagination.
  • createUser: Sends POST requests to create a user.
  • updateUser: Supports both PUT (complete update) and PATCH (partial update) operations.
  • deleteUser: Deletes a user by ID.
Each method uses handleRequest to standardise request execution and error processing.
GET request: Fetch users
  • ScriptRunner Connect: In the previous post, we hardcoded the GET request to fetch the second page of users using the ReqRes API. This script used query parameters like page for pagination.
  • Managed API: The getUsers(pageNumber) method in the ReqResAPI class implements this logic, dynamically accepting the pageNumber as a parameter. It uses a private handleRequest method to manage API responses and errors uniformly, improving flexibility and error handling.

src/ReqResAPI.ts

// Implement API Methods
/**
* Fetches a paginated list of users.
* @param pageNumber - The page number to fetch.
* @returns A promise that resolves to an object containing user data or an error message.
*/
async getUsers(pageNumber: number): Promise<{ data?: PaginatedUsersResponse; error?: string }> {
	return this.handleRequest<PaginatedUsersResponse>(() => this.client.fetch(`/users?page=${pageNumber}`));
}
POST request: Create a user
  • ScriptRunner Connect: The script demonstrated creating a user with hardcoded data, including name and job. While functional, it lacked flexibility for dynamic input.
  • Managed API: The createUser(data: CreateUserRequest) method in the managed API allows passing dynamic user data as parameters, improving usability. It ensures consistency with server responses by leveraging TypeScript's interfaces.

src/ReqResAPI.ts

// Implement API Methods
/**
* Creates a new user.
* @param data - The user details including name and job title.
* @returns A promise that resolves to the created user data or an error message.
*/
async createUser(data: CreateUserRequest): Promise<{ data?: CreateUserResponse; error?: string }> {
	return this.handleRequest<CreateUserResponse>(() =>
		this.client.fetch('/users', {
			method: 'POST',
			headers: { 'Content-type': 'application/json' },
			body: JSON.stringify(data),
		})
	);
}
PUT & PATCH request: (Partially) update a user
  • ScriptRunner Connect: The PUT script updated a user with static data and a hardcoded userId, and the PATCH script updated only specific fields using hardcoded values.
  • Managed API: The updateUser(userId, data, method) method adds dynamic capabilities by allowing both PUT(complete update) and PATCH (partial update) operations. It accepts a flexible method parameter to define the type of update, consolidating the logic for both operations.

src/ReqResAPI.ts

// Implement API Methods
/**
* Updates an existing user.
* @param userId - The ID of the user to update.
* @param data - The new user details including name and job title.
* @param method - Define how update should happen, in fully (PUT) or partial (PATCH)
* @returns A promise that resolves to the updated user data or an error message.
*/
async updateUser(userId: number, data: CreateUserRequest, method: 'PUT' | 'PATCH'): Promise<{
	data?:
	UpdateUserResponse; error?: string
}> {
	return this.handleRequest<UpdateUserResponse>(() =>
		this.client.fetch(`/users/${userId}`, {
			method: method,
			headers: { 'Content-type': 'application/json' },
			body: JSON.stringify(data),
		})
	);
}
DELETE request: Remove a user
  • ScriptRunner Connect: The script was hardcoded to delete a specific user by userId, which limited its usability.
  • Managed API: The deleteUser(userId) method in the managed API improves on this by accepting a dynamic userId, ensuring it can delete any user specified at runtime. It also checks for expected status codes, maintaining consistency with best practices.
// Implement API Methods
/**
* Deletes a user by ID.
* @param userId - The ID of the user to delete.
* @returns A promise that resolves to `true` if the user was deleted successfully or an error message.
*/
async deleteUser(userId: number): Promise<{ data?: boolean; error?: string }> {
	return this.handleRequest<boolean>(async () => {
		const response = await this.client.fetch(`/users/${userId}`, { method: 'DELETE' });
		if (response.ok) {
			return new Response(JSON.stringify(true)); // Simulate a JSON `true` response for consistency
		}
		return response;
	});
}

Export everything in index.ts

The index.ts file acts as the module's entry point:
  • Exports the ReqResAPI class and all defined types.
  • Facilitates easy import of the API functionality in other projects by bundling all exports in a single file.

src/index.ts

export { ReqResAPI } from './ReqResAPI';
export * from './ReqResType';

Build the project

Run the following command:
npm run build
This compiles TypeScript into JavaScript and generates the dist directory.

Create an npm account

  1. Visit npmjs.com.
  2. Sign up and verify your email.
  3. Log in via the terminal: npm login

Publish the package

This step involves making the API client publicly available as an npm package:
  1. Build: Transpile TypeScript to JavaScript.
  2. Setup: Use package.json to configure package metadata, including dependencies and publish settings.
  3. Publish: Upload the package to npm using the npm publish command to make it accessible to other developers and applications.
Publishing allows the managed API to be reused across multiple projects, ensuring a scalable and maintainable solution.

Use in ScriptRunner Connect

Add dependency

  1. From your workspace in ScriptRunner Connect, click on the three dots in the top right-hand side of your screen and choose Package Manager.
  2. Select the Third Party Packages tab
  3. Under Add an unverified npm package search for reqres-api and click Add.

Import and use

  1. Navigate to the Scripts tab in your workspace.
  2. Click + to create a new script, give it a name (e.g., "managedAPI"), and select TypeScript as the language.
  3. Import API Connection.
  4. Import the Managed API you have published in npm.

managedAPI

// API Connection
import ReqresInClient from './api/generic';
// Managed API
import { ReqResAPI } from 'reqres-api'

export default async function (event: any, context: Context): Promise<void> {
	console.log('Script triggered', event, context);
	// Create a Managed API instance
	const managedAPI = new ReqResAPI(ReqresInClient);
	// Fetch Users
	// Fetch All Users
	// Create a User
	// Update a User
	// Partially Update a User
	// Remove a User
}

GET request: Fetch users

Explanation:
  1. Initialisation: Searches for users from a given page, e.g 2
  2. Error handling: If an error occurs while fetching a page, the function logs the error and returns the collected users so far.
// Fetch Users
// Fetch users from page 2
const users = await managedAPI.getUsers(2);
console.log("Users", users);

GET request: Fetch all users

To fetch all users using recursion, you can create a function that calls itself until all pages are fetched. Here's how you can implement this:
Explanation:
  1. Recursive function:
    1. fetchAllUsers(currentPage, totalPages, users) handles the recursive logic.
    2. Parameters:
      1. currentPage: Tracks the current page.
      2. totalPages: Total number of pages to fetch (determined dynamically from the first response).
      3. users: Accumulates user data across recursive calls.
  2. Base case: If currentPage exceeds totalPages, the function stops and returns the accumulated users array.
  3. Recursive case: The function calls itself with currentPage + 1 until all pages are fetched.
  4. Initialisation: The recursion starts with currentPage = 1 and totalPages undefined. The totalPages value is set based on the first API response.
  5. Error handling: If an error occurs while fetching a page, the function logs the error and returns the collected users so far.
This recursive approach dynamically handles pagination and keeps the logic clean by leveraging function calls for each page fetch.
// Fetch All Users
// Function to recursively fetch all users
const fetchAllUsers = async (currentPage: number, totalPages?: number, users: any[] = []): Promise<any[]> => {
	// Fetch users for the current page
	const response = await managedAPI.getUsers(currentPage);
	
	if (response.error) {
		console.error(`Error fetching users on page ${currentPage}:`, response.error);
		return users; // Return the collected users even if there's an error
	}

	const { data } = response;
	if (data) {
		// Append the current page's users to the collected list
		const updatedUsers = [...users, ...data.data];

		// If totalPages is undefined (first call), initialise it
		if (!totalPages) {
			totalPages = data.total_pages;
		}

		// Base case: If we have fetched the last page, return all users
		if (currentPage >= totalPages) {
			return updatedUsers;
		}

		// Recursive case: Fetch the next page
		return fetchAllUsers(currentPage + 1, totalPages, updatedUsers);
	}

	// Return collected users if no data is returned
	return users;
};

// Start fetching from page 1
const allUsers = await fetchAllUsers(1);
console.log('All Users:', allUsers);

POST request: Create a user

To call the create a user method using the managedAPI instance, you can use the createUser method provided by the ReqResAPI class. This method expects a parameter containing the user’s details, such as their name and job.
Explanation:
  1. User data:
    1. Define the userData object with the necessary fields:
      1. name: The name of the user.
      2. job: The job title of the user.
  2. Method call: Use await managedAPI.createUser(userData) to create a user. The method sends a POST request to the API with the provided user data.
  3. Response handling: Check if the response.error exists to handle any errors during the API call.
    1. If no error, access the response.data to retrieve the details of the created user, including their id and createdAt timestamp.
// Create a User
// Define the user details to create
const userData = {
	name: 'Jane Doe',
	job: 'Software Developer'
};

// Call the createUser method
const response = await managedAPI.createUser(userData);
if (response.error) {
	console.error('Error creating user:', response.error);
} else {
	console.log('User Created Successfully:', response.data);
}
Sample output:
If the user is created successfully, the console might log:
User Created Successfully: {
    id: "573",
    name: "Jane Doe",
    job: "Software Developer",
    createdAt: "2024-12-03T12:34:56.789Z"
}
This makes it easy to dynamically create users by varying the userData object as needed.

PUT request: Update a user

To update a user using the PUT method with the managedAPI instance, you can use the updateUser method in the ReqResAPI class. This method requires the userId to identify the user and the updated details for the user.
Explanation:
1. User identification:
userId: Specify the ID of the user you want to update.
2. Updated data:
updatedData: An object containing the new name and job values.
3. Method call:
Use await managedAPI.updateUser(userId, updatedData, 'PUT') to send a complete update to the user data using the PUT method.
4. Response handling:
  1. Check if response.error exists to identify any issues during the request.
  2. Access response.data to view the updated user details if the request is successful.
// Update a User
// Define the user ID and updated details
const userId = 2; // Replace with the ID of the user to update
const updatedData = {
	name: 'John Smith',
	job: 'Senior Developer'
};

// Call the updateUser method with PUT
const response = await managedAPI.updateUser(userId, updatedData, 'PUT');
if (response.error) {
	console.error('Error updating user:', response.error);
} else {
	console.log('User Updated Successfully:', response.data);
}
Sample output:
If the user is updated successfully, the console might log:
User Updated Successfully: {
    name: "John Smith",
    job: "Senior Developer",
    updatedAt: "2024-12-03T12:34:56.789Z"
}
This approach ensures that the entire user object is replaced with the provided data when using the PUT method.

PATCH request: Partially update a user

To update a user using the PATCH method with the managedAPI instance, you can use the updateUser method in the ReqResAPI class. This method is flexible and supports partial updates when you specify PATCH as the HTTP method.
Explanation:
  1. User identification:
userId: Specify the ID of the user you want to partially update.
  1. Partial update data:
updatedData: An object containing only the fields you wish to update (e.g., job in this example).
  1. Method call:
Use await managedAPI.updateUser(userId, updatedData, 'PATCH') to send a partial update to the user data.
  1. Response handling:
    1. Check response.error to handle errors during the request.
    2. Access response.data to view the partially updated user details if the request is successful.
// Partially Update a User
// Define the user ID and partial data to update
const userId = 3; // Replace with the ID of the user to update
const updatedData = {
	job: 'Lead Developer' // Only updating the job field
};

// Call the updateUser method with PATCH
const response = await managedAPI.updateUser(userId, updatedData, 'PATCH');
if (response.error) {
	console.error('Error partially updating user:', response.error);
} else {
	console.log('User Partially Updated Successfully:', response.data);
}
Sample Output:
If the user is updated successfully, the console might log:
User Partially Updated Successfully: {
    name: "Jane Doe", // This remains unchanged
    job: "Lead Developer",
    updatedAt: "2024-12-03T12:34:56.789Z"
}
This approach ensures that only the specified fields in updatedData are updated while other user details remain intact. Using PATCH is ideal for scenarios where you need to modify only part of the user's data.

DELETE request: Remove a user

To remove a user using the DELETE method with the managedAPI instance, you can use the deleteUser method in the ReqResAPI class. This method requires the userId of the user you wish to delete.
Explanation:
1. User identification:
userId: Specify the ID of the user you want to delete.
2. Method call:
Use await managedAPI.deleteUser(userId) to send a DELETE request to remove the user.
3. Response handling:
  1. Check response.error to identify any issues during the request.
  2. If the request is successful, confirm the user was removed by logging a success message.
// Remove a User
// Define the user ID to delete
const userId = 4; // Replace with the ID of the user to delete
// Call the deleteUser method
const response = await managedAPI.deleteUser(userId);
if (response.error) {
	console.error('Error removing user:', response.error);
} else {
	console.log(`User with ID ${userId} removed successfully.`);
}
Sample output:
If the user is removed successfully, the console might log:
User with ID 4 removed successfully.
If there’s an error (e.g., the user does not exist), the error will be logged:
Error removing user: Unexpected response code: 404 - User not found
This approach ensures that you can dynamically delete any user by specifying their userId.

Conclusion

Congratulations! By following this tutorial series, you’ve learned how to connect anything with a REST API using ScriptRunner Connect. Here's a recap of the key takeaways:
1. Getting to grips with the basics of REST APIs
  • You gained a solid understanding of REST APIs and how they allow disparate systems to communicate using standard HTTP methods.
2. REST APIs made easy with ScriptRunner Connect
  • You explored how to automate REST API interactions with ScriptRunner Connect, creating scripts to perform GET, POST, PUT, PATCH, and DELETE operations.
  • You also learned how to enhance automation with scheduled triggers.
3. Building a managed API
  • This final post demonstrated how to encapsulate API logic into a reusable and scalable TypeScript package, enabling portability and maintainability.
  • You learned how to structure your project, implement robust API methods, and publish your managed API to npm.

What’s next?

For those looking to take their managed API implementation to the next level, consider enhancing the ReqResAPI class by transitioning the private client type from any to GenericAppApiCore. This improvement involves:
  • Updating the imports to include:
//import { GenericAppApiCore } from "@managed-api/generic-core";
  • Modifying the methods for GET, POST, PUT, PATCH, and DELETE to handle the stricter typing and return formats required by GenericAppApi Core.
This enhancement will improve type safety and ensure compatibility with more advanced automation workflows.

Let’s hear your feedback!

Have you completed the series and have ideas or questions about implementing a managed API or using GenericAppApiCore? We’d love to hear from you! Send us a message or reach out on social media to share your thoughts on this tutorial.
Keep coding, keep automating, and keep connecting!
Published on 5 March 2025
Authors
Photo of Rafael Pinto Sperafico
Rafael Pinto Sperafico
Ambientia logo
Ambientia
Share this tutorial
Written by
Photo of Rafael Pinto Sperafico
Rafael Pinto Sperafico
Senior Software Developer
I’m part of the dynamic teams at Ambientia, working at the intersection of innovation and technology. With a passion for APIs, automation, and developer enablement, I’m excited to guide you through the fundamentals of REST APIs and how they can transform the way applications communicate and integrate by using ScriptRunner Connect.
Connect with Rafael on LinkedIn.

Ambientia logo
Ambientia
Ambientia is a Finnish technology consultancy, partners with organizations to create smart digital solutions. With a focus on collaboration, automation, and user-centric design, Ambientia brings cutting-edge technologies to life, helping companies across various industries to innovate and grow sustainably.
Visit Ambientia’s website, or follow them on LinkedIn, Instagram, YouTube or Facebook.
Related content
Discover more like this
A person rests on a pillow while a computer is connected to a server

Getting to grips with the basics of REST APIs (From REST consumer to API provider 1/3)

Welcome to the From REST consumer to API provider guest tutorial. In this three-part series, Rafael Pinto Sperafico from The Adaptavist Group partner, Ambientia, will guide you through the world of REST APIs.
by Rafael Pinto Sperafico
on 5 Mar 2025
A person points to the ScriptRunner Connect logo which connects a webpage to a server

REST APIs made easy with ScriptRunner (From REST consumer to API provider 2/3)

Welcome to the From REST consumer to API provider guest tutorial. In this three-part series, Rafael Pinto Sperafico from The Adaptavist Group partner, Ambientia, will guide you through the world of REST APIs. In this tutorial we’ll explore how to automate REST API calls using ScriptRunner Connect.
by Rafael Pinto Sperafico
on 5 Mar 2025