Thank you for your interest in contributing to the ArcXP SDK! This guide will help you understand the project architecture and contribution workflow.
- Project Architecture
- Getting Started
- Adding New Methods
- Type System
- Quality Standards
- Local Development
The foundation of all API modules, providing common HTTP client infrastructure:
Key Features:
- Axios-based HTTP client with rate limiting (default 10 RPS, configurable)
- Exponential backoff retry logic for transient failures (5 retries)
- Automatic error wrapping with
ArcErrorclass - Built-in rate limit headers tracking (
arcpub-ratelimit-remaining,arcpub-ratelimit-reset) - Dynamic base URL construction:
https://api.{org}.arcpublishing.com/{apiPath}
Retry Logic: Automatically retries on:
- 408 Request Timeout
- 429 Too Many Requests
- 500 Internal Server Error
- 502 Bad Gateway
- 503 Service Unavailable
- 504 Gateway Timeout
Custom error class that captures:
- Response data, status, and config
- Rate limit information
- Service name for debugging
- Original stack trace preservation
Each API service follows a consistent two-file pattern:
src/api/{service}/
├── index.ts # Implementation (extends ArcAbstractAPI)
└── types.ts # Request/Response types
Implementation Pattern:
import type { GlobalType } from '../../types/entity';
import { type ArcAPIOptions, ArcAbstractAPI } from '../abstract-api.js';
import type { LocalParamsType } from './types.js';
export class ArcServiceName extends ArcAbstractAPI {
constructor(options: ArcAPIOptions) {
super({ ...options, apiPath: 'service/v1' });
}
async methodName(params: LocalParamsType): Promise<GlobalType> {
const { data } = await this.client.get('/endpoint', { params });
return data;
}
}1. Global Types (./src/types/)
- Generated from ANS (ArcXP Native Schema) using
json-schema-to-typescript ⚠️ Do not modify manually - these are auto-generated- Core entities:
AStory,AnImage,AGallery,AVideo, etc. - Shared across all APIs
- Regenerate with:
pnpm run gen:ts
2. Local Types (./src/api/{service}/types.ts)
- Request parameters (always local)
- Response types (only if not in global types)
- Service-specific enums and utility types
Need a type?
├─ Is it a request parameter?
│ └─ ✅ Define locally in {service}/types.ts
│
├─ Is it a response?
│ ├─ Check ./src/types/ first
│ ├─ Exists? → ✅ Import and use
│ └─ Doesn't exist? → ✅ Define locally
│
└─ Unclear from spec?
└─ Ask for clarification
Golden Rule: Never duplicate type definitions. Always reuse global types when available.
- Node.js >= 22.x
- pnpm >= 10.x
pnpm install# Build the project
pnpm run build
# Format code
pnpm run format
# Lint and auto-fix
pnpm run lint
# Run tests
pnpm run test
# Generate coverage report
pnpm run coverage
# Generate TypeScript types from JSON schemas
pnpm run gen:ts
# Create a changeset
pnpm run csFetch the OpenAPI specification for the target API:
// Use the sdk-creator tool if available
sdk-creator:fetch_arcxp_api_spec({ api_name: "content_api" })Or manually review the ArcXP API documentation.
- Compare spec endpoints with current implementation
- Identify missing methods
- Note parameter and response structures
- Check for similar existing patterns in the codebase
In {service}/types.ts:
// Always define request parameters locally
export type NewMethodParams = {
/** Required website identifier */
website: string;
/** Optional filter parameter */
filter?: string;
/** Pagination offset */
offset?: number;
};
// For responses - check global types first!
// If exists: import type { AStory } from '../../types/story';
// If not, define locally:
export type NewMethodResponse = {
items: AStory[];
total: number;
next?: string;
};In {service}/index.ts:
async newMethod(params: NewMethodParams): Promise<NewMethodResponse> {
const { data } = await this.client.get('/endpoint', { params });
return data;
}Ensure types are exported from the service's types.ts and re-exported in src/index.ts:
// In src/index.ts
export * from './api/{service}/types.js';Create a changeset describing your additions:
pnpm run csFollow the prompts to describe your changes. This will generate a markdown file in .changeset/ directory.
async getItem(id: string): Promise<AStory> {
const { data } = await this.client.get(`/items/${id}`);
return data;
}async search(params: SearchParams): Promise<SearchResponse> {
const { data } = await this.client.get('/search', { params });
return data;
}async getByIds(params: GetByIdsParams): Promise<Response> {
const { data } = await this.client.get('/items', {
params: { ...params, ids: params.ids.join(',') }
});
return data;
}async create(payload: CreatePayload): Promise<AStory> {
const { data } = await this.client.post('/items', payload);
return data;
}async uploadFile(
stream: ReadStream,
options?: { filename?: string }
): Promise<AnImage> {
const form = new FormData();
form.append('file', stream, {
filename: options?.filename || 'file.jpg',
contentType: 'application/octet-stream',
});
const { data } = await this.client.post('/upload', form, {
headers: form.getHeaders()
});
return data;
}async createItem<
P extends CreatePayload,
R = P extends TypeA ? ResponseA : ResponseB
>(payload: P): Promise<R> {
const { data } = await this.client.post('/items', payload);
return data as R;
}async deleteItem(id: string, type = 'story'): Promise<void> {
await this.client.delete(`/${type}/${id}`);
}ArcXP Native Schema (ANS) types are pre-generated and located in ./src/types/:
story.ts- Story-related types (AStory,AnImage, etc.)gallery.ts- Gallery types (AGallery)video.ts- Video types (AVideo)author.ts- Author typessection.ts- Section typescontent-elements.ts- Content element types
// Importing global types
import type { AStory } from '../../types/story';
import type { AGallery } from '../../types/gallery';
import type { AnImage } from '../../types/story';
// Importing local types
import type { GetStoryParams, SearchResponse } from './types.js';If the OpenAPI spec provides minimal information like:
{
"type": "story"
}- Check
./src/types/for a matching global type (e.g.,AStory) - If found, import and use it
- If unclear, ask for clarification before proceeding
The project uses Biome for formatting and linting. Configuration is in biome.json:
- Quotes: Single quotes
- Semicolons: Always
- Trailing commas: ES5 style
- Line width: 120 characters
- Indentation: 2 spaces
- Arrow parentheses: Always
Run formatting before committing:
pnpm run format
pnpm run lint- ✅ Strict mode enabled - No implicit any
- ✅ Explicit return types for public methods
- ✅ JSDoc comments for complex types and methods
- ✅ Async/await throughout (no callbacks)
- ✅ Proper error handling (errors wrapped in
ArcError) - ✅ No type assertions unless absolutely necessary
-
DRY (Don't Repeat Yourself)
- Reuse global types
- Extract common patterns into utilities
-
Single Responsibility
- Each API module handles one service
- Each method does one thing well
-
Consistent Naming
- Methods:
camelCase - Types/Interfaces:
PascalCase - Constants:
UPPER_SNAKE_CASE - Private properties: prefix with
_or mark asprivate
- Methods:
-
Type Safety
- Avoid
anyin production code - Use proper generic constraints
- Leverage TypeScript's type inference
- Avoid
-
Documentation
- Add JSDoc comments for public APIs
- Document complex type relationships
- Explain non-obvious parameter usage
Write tests for new functionality:
import { describe, it, expect } from 'vitest';
import { ArcServiceName } from './index.js';
describe('ArcServiceName', () => {
it('should fetch item by id', async () => {
// Test implementation
});
});Run tests:
pnpm run test
pnpm run coverageWhen developing locally and you want to test your changes in another project without publishing:
# In the arcxp-sdk-ts directory
pnpm link --global
# In your consuming project
pnpm link --global @code.store/arcxp-sdk-tsThis creates a symlink to your local development version, allowing you to test changes in real-time.
# In arcxp-sdk-ts directory
pnpm run buildYour consuming project will now use the updated built version.
When you're done testing locally:
# In your consuming project
pnpm unlink --global @code.store/arcxp-sdk-ts
# Reinstall the published version
pnpm install @code.store/arcxp-sdk-ts- Make changes to source files in
src/ - Build the project:
pnpm run build - Test locally in a linked project
- Run tests:
pnpm run test - Format code:
pnpm run format && pnpm run lint - Create changeset:
pnpm run cs - Commit changes with conventional commit messages
- Submit PR with clear description
- TypeScript 5.3+ with strict mode
- Module System: CommonJS (output)
- Formatter/Linter: Biome
- Test Runner: Vitest
- Versioning: Changesets
- Package Manager: pnpm
tsconfig.json- TypeScript compiler configurationbiome.json- Code formatting and linting rulespackage.json- Project metadata and scriptsvite.config.ts- Vitest configuration
The build process:
- Compiles TypeScript to CommonJS in
./dist/ - Generates type declarations (
.d.tsfiles) - Creates source maps for debugging
Implemented Services:
- Author
- Content
- Content Ops
- Custom
- Draft
- Global Settings
- Identity
- IFX
- Migration Center
- Photo Center
- Redirect
- Retail Events (WebSockets)
- Sales
- Signing Service
- Site
- Tags
- Websked
This project follows Semantic Versioning and uses Changesets for version management.
After making changes:
pnpm run csThis will:
- Prompt you to select the change type (major/minor/patch)
- Ask for a description of the changes
- Create a markdown file in
.changeset/
The changeset will be used to:
- Generate the changelog
- Determine the next version number
- Document changes for users
Patch (0.0.X) - Bug fixes and minor tweaks:
- Bug fixes
- Documentation updates
- Internal refactoring
Minor (0.X.0) - New features (backwards compatible):
- New API methods
- New optional parameters
- Enhanced functionality
Major (X.0.0) - Breaking changes:
- Removed or renamed methods
- Changed method signatures
- Modified return types
Let's walk through adding a new method to the Content API:
Review the ArcXP Content API documentation for the endpoint /v4/stories/bulk.
// In src/api/content/types.ts
export type BulkGetStoriesParams = {
/** Website identifier */
website: string;
/** Comma-separated list of story IDs */
ids: string[];
/** Whether to return published or draft stories */
published?: boolean;
/** Fields to include in response */
included_fields?: string;
};
export type BulkGetStoriesResponse = {
stories: AStory[];
errors?: Array<{
id: string;
error: string;
}>;
};// In src/api/content/index.ts
async bulkGetStories(params: BulkGetStoriesParams): Promise<BulkGetStoriesResponse> {
const { data } = await this.client.post('/stories/bulk', {
ids: params.ids,
website: params.website,
published: params.published ?? true,
included_fields: params.included_fields,
});
return data;
}pnpm run build
pnpm link --global
# In your test project
pnpm link --global @code.store/arcxp-sdk-ts
# Test the new method
import { ArcAPI } from '@code.store/arcxp-sdk-ts';
const api = ArcAPI({
credentials: {
organizationName: 'your-org',
accessToken: 'your-token'
}
});
const result = await api.Content.bulkGetStories({
website: 'my-site',
ids: ['story1', 'story2'],
});pnpm run cs
# Select: minor
# Description: "Add bulkGetStories method to Content API"Include in your PR description:
- What was added
- Why it was added
- Any breaking changes
- Example usage
- Issues: Open an issue on GitHub
- Documentation: Check the README.md
- ArcXP Docs: Visit ArcXP Documentation
This project is licensed under the MIT License - see the LICENSE file for details.