Skip to content

Issue 812#1099

Open
ramakanth98 wants to merge 4 commits into
mainfrom
issue-812
Open

Issue 812#1099
ramakanth98 wants to merge 4 commits into
mainfrom
issue-812

Conversation

@ramakanth98
Copy link
Copy Markdown
Collaborator

@ramakanth98 ramakanth98 commented Jun 3, 2026

Summary

Closes #812

Introduces a common interface for front-end API requests, replacing scattered axios calls with a tested, resource-scoped
wrapper — following the same class + custom error pattern used in shared/presenters/payment-presenter.js.

  • Adds src/api/index.js exporting API (takes a resource path and optional version, exposes get/post/put/delete) and APIError (extends Error, carries HTTP status)
  • Adds src/api/__tests__/api.test.js with 8 Jest unit tests covering URL construction, success/failure for get and post, and error status propagation

Usage

// Versioned route
const letterTemplates = new API('/letter_templates', 'v1')
const data = await letterTemplates.post('/render', { templateId, mergeVariables })

// Unversioned route
const representatives = new API('/representatives')
const reps = await representatives.get(`/${postalCode}`)

Test plan

  • npx jest src/api/__tests__/api.test.js --no-coverage → 8 passed
  • Full test suite passes: npm test

Notes

This PR lays the foundation. Existing axios call sites (SearchReps, DonateMoney, SignName, etc.) are intentionally left for
follow-up migration — per the issue spec: "Don't worry about moving every api over, just the new stuff."

Summary by CodeRabbit

  • Documentation

    • Added planning and design specifications for a new API wrapper implementation.
  • Chores

    • Introduced an API wrapper with error handling and HTTP method support.
    • Added comprehensive unit tests for API wrapper behavior.
    • Removed unused import from router configuration.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 3, 2026

emote

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a class-based axios API wrapper (API) and custom error class (APIError) to standardize front-end HTTP requests, along with comprehensive Jest unit tests and design documentation. An unused import is also removed.

Changes

API Wrapper Introduction

Layer / File(s) Summary
API design spec and implementation plan
docs/superpowers/specs/2026-06-03-api-wrapper-design.md, docs/superpowers/plans/2026-06-03-api-wrapper.md
Design specification and implementation plan outline the APIError and API class contracts, Jest test expectations, and two example refactoring call sites for migration from direct axios usage to the new wrapper pattern.
API wrapper and error class implementation
src/api/index.js
New module exports APIError (subclassing Error with optional HTTP status) and API class that builds version-aware baseUrl from path and version, implements get/post/put/delete methods via axios, returns response.data on success, and converts request failures into APIError with message and optional status.
Jest unit tests for API wrapper
src/api/__tests__/api.test.js
Test suite with mocked axios validates baseUrl construction with and without optional version prefix, confirms get() and post() methods return response.data on success, verifies error handling throws APIError and exposes HTTP status on failure.
Unused import removal
src/router/index.js
Removes unused Home import from router configuration.

🎯 2 (Simple) | ⏱️ ~12 minutes

🐰 A wrapper comes to wrap today,
With axios tamed in every way,
Tests ensure the errors stay clear,
Now async calls are structured here,
Version paths and status codes so dear!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Issue 812' is vague and does not clearly describe the actual changes made; it only references the issue number without indicating what was implemented. Rename the title to something more descriptive like 'Add API wrapper class with custom error handling' to clearly communicate the main change.
Out of Scope Changes check ❓ Inconclusive The removal of the unused 'Home' import from src/router/index.js appears unrelated to the API wrapper implementation; clarification is needed on whether this was intentional or accidental. Confirm whether the Home import removal in src/router/index.js is intentional or should be reverted as an out-of-scope change.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue #812: creates an API class accepting path and optional version, implements APIError extending Error with HTTP status, provides comprehensive Jest tests, and avoids unnecessary migration of existing call sites.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-812

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/api/index.js (1)

12-14: ⚡ Quick win

Consider validating or normalizing the path parameter.

The constructor doesn't validate that path starts with /. If a caller passes 'campaigns' instead of '/campaigns', the resulting baseUrl would be malformed (e.g., /api/v1campaigns). While all current examples use the correct format, adding defensive validation or automatic prepending would prevent future bugs.

🛡️ Proposed defensive fix
  constructor(path, version) {
+   const normalizedPath = path.startsWith('/') ? path : `/${path}`
-   this.baseUrl = version ? `/api/${version}${path}` : `/api${path}`
+   this.baseUrl = version ? `/api/${version}${normalizedPath}` : `/api${normalizedPath}`
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/index.js` around lines 12 - 14, The constructor currently builds
this.baseUrl from the path without ensuring it begins with '/', so passing
"campaigns" produces a malformed URL; normalize/validate the path at the start
of the constructor (e.g., if path is falsy or not a string throw or default to
'/', and if it doesn't startWith('/') prepend '/'), then compute this.baseUrl
exactly as before using the normalized path and the version parameter; reference
the constructor and this.baseUrl to locate where to add the normalization.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-06-03-api-wrapper.md`:
- Line 184: The expected test summary string "Tests: 7 passed, 7 total" in the
plan should be updated to match the actual tests — change it to "Tests: 8
passed, 8 total"; confirm by comparing the test file
src/api/__tests__/api.test.js which contains 8 tests (2 URL construction tests +
3 get() tests + 3 post() tests) so the plan's expected output string must
reflect 8 total tests.

In `@docs/superpowers/specs/2026-06-03-api-wrapper-design.md`:
- Around line 85-91: The fenced code block showing the file structure is missing
a language identifier; update that fenced block (the triple-backtick block
containing "src/ api/ index.js ...") to include a language token such as
plaintext (e.g., ```plaintext) so the snippet gets proper syntax highlighting
and clarity in the docs.

In `@src/api/__tests__/api.test.js`:
- Around line 1-82: Add unit tests for API.put and API.delete mirroring the
existing get/post suites: create tests that (1) mock axios.put/axios.delete to
resolve with { data: ... } and assert API.put(path, body) and API.delete(path)
return response.data, (2) mock axios.put/axios.delete to reject with { message,
response: { status } } and assert the calls reject with APIError, and (3) assert
the thrown APIError carries the HTTP status (use rejects.toMatchObject({ status
})) — place these new describe/it blocks for API.put and API.delete after the
current post() tests and reuse the existing jest.mock('axios') setup and API
constructor patterns.

---

Nitpick comments:
In `@src/api/index.js`:
- Around line 12-14: The constructor currently builds this.baseUrl from the path
without ensuring it begins with '/', so passing "campaigns" produces a malformed
URL; normalize/validate the path at the start of the constructor (e.g., if path
is falsy or not a string throw or default to '/', and if it doesn't
startWith('/') prepend '/'), then compute this.baseUrl exactly as before using
the normalized path and the version parameter; reference the constructor and
this.baseUrl to locate where to add the normalization.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0f5ba642-9238-41e1-916d-984266a154cb

📥 Commits

Reviewing files that changed from the base of the PR and between c186d01 and f1266c7.

📒 Files selected for processing (5)
  • docs/superpowers/plans/2026-06-03-api-wrapper.md
  • docs/superpowers/specs/2026-06-03-api-wrapper-design.md
  • src/api/__tests__/api.test.js
  • src/api/index.js
  • src/router/index.js
💤 Files with no reviewable changes (1)
  • src/router/index.js

npx jest src/api/__tests__/api.test.js --no-coverage
```

Expected output: `Tests: 7 passed, 7 total`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct the expected test count.

The plan states "Tests: 7 passed, 7 total", but the actual test file src/api/__tests__/api.test.js contains 8 tests (2 URL construction tests + 3 get() tests + 3 post() tests).

📝 Proposed fix
-Expected output: `Tests: 7 passed, 7 total`
+Expected output: `Tests: 8 passed, 8 total`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Expected output: `Tests: 7 passed, 7 total`
Expected output: `Tests: 8 passed, 8 total`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-06-03-api-wrapper.md` at line 184, The expected
test summary string "Tests: 7 passed, 7 total" in the plan should be updated to
match the actual tests — change it to "Tests: 8 passed, 8 total"; confirm by
comparing the test file src/api/__tests__/api.test.js which contains 8 tests (2
URL construction tests + 3 get() tests + 3 post() tests) so the plan's expected
output string must reflect 8 total tests.

Comment on lines +85 to +91
```
src/
api/
index.js ← new
__tests__/
api.test.js ← new
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add language identifier to fenced code block.

The code block showing the file structure is missing a language identifier. Adding one improves syntax highlighting and clarity.

📝 Proposed fix
-```
+```plaintext
 src/
   api/
     index.js                  ← new
     __tests__/
       api.test.js             ← new
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 85-85: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @docs/superpowers/specs/2026-06-03-api-wrapper-design.md around lines 85 -
91, The fenced code block showing the file structure is missing a language
identifier; update that fenced block (the triple-backtick block containing "src/
api/ index.js ...") to include a language token such as plaintext (e.g.,

docs.

Comment on lines +1 to +82
import axios from 'axios'
import { API, APIError } from '../index'

jest.mock('axios')

afterEach(() => {
jest.clearAllMocks()
})

describe('API — URL construction', () => {
test('builds base URL with version', () => {
const api = new API('/campaigns', 'v1')
expect(api.baseUrl).toBe('/api/v1/campaigns')
})

test('builds base URL without version', () => {
const api = new API('/representatives')
expect(api.baseUrl).toBe('/api/representatives')
})
})

describe('API — get()', () => {
test('returns response.data on success', async () => {
const mockData = { id: 1, name: 'test' }
axios.get.mockResolvedValue({ data: mockData })

const api = new API('/campaigns')
const result = await api.get()
expect(result).toEqual(mockData)
})

test('throws APIError on failure', async () => {
axios.get.mockRejectedValue({
message: 'Not found',
response: { status: 404 }
})

const api = new API('/campaigns')
await expect(api.get()).rejects.toThrow(APIError)
})

test('APIError carries HTTP status', async () => {
axios.get.mockRejectedValue({
message: 'Not found',
response: { status: 404 }
})

const api = new API('/campaigns')
await expect(api.get()).rejects.toMatchObject({ status: 404 })
})
})

describe('API — post()', () => {
test('returns response.data on success', async () => {
const mockData = { letter: '<p>Hello</p>' }
axios.post.mockResolvedValue({ data: mockData })

const api = new API('/letter_templates', 'v1')
const result = await api.post('/render', { templateId: 1 })
expect(result).toEqual(mockData)
})

test('throws APIError on failure', async () => {
axios.post.mockRejectedValue({
message: 'Server error',
response: { status: 500 }
})

const api = new API('/letter_templates', 'v1')
await expect(api.post('/render', {})).rejects.toThrow(APIError)
})

test('APIError carries HTTP status', async () => {
axios.post.mockRejectedValue({
message: 'Server error',
response: { status: 500 }
})

const api = new API('/letter_templates', 'v1')
await expect(api.post('/render', {})).rejects.toMatchObject({ status: 500 })
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add test coverage for put() and delete() methods.

The test suite covers get() and post() comprehensively, but the put() and delete() methods implemented in src/api/index.js are not tested at all. This creates a significant gap in test coverage for public API methods.

✅ Proposed tests to add

Add these test suites after the existing post() tests:

+describe('API — put()', () => {
+  test('returns response.data on success', async () => {
+    const mockData = { id: 1, updated: true }
+    axios.put.mockResolvedValue({ data: mockData })
+
+    const api = new API('/campaigns')
+    const result = await api.put('/123', { name: 'Updated' })
+    expect(result).toEqual(mockData)
+  })
+
+  test('throws APIError on failure', async () => {
+    axios.put.mockRejectedValue({
+      message: 'Update failed',
+      response: { status: 400 }
+    })
+
+    const api = new API('/campaigns')
+    await expect(api.put('/123', {})).rejects.toThrow(APIError)
+  })
+
+  test('APIError carries HTTP status', async () => {
+    axios.put.mockRejectedValue({
+      message: 'Update failed',
+      response: { status: 400 }
+    })
+
+    const api = new API('/campaigns')
+    await expect(api.put('/123', {})).rejects.toMatchObject({ status: 400 })
+  })
+})
+
+describe('API — delete()', () => {
+  test('returns response.data on success', async () => {
+    const mockData = { deleted: true }
+    axios.delete.mockResolvedValue({ data: mockData })
+
+    const api = new API('/campaigns')
+    const result = await api.delete('/123')
+    expect(result).toEqual(mockData)
+  })
+
+  test('throws APIError on failure', async () => {
+    axios.delete.mockRejectedValue({
+      message: 'Delete failed',
+      response: { status: 403 }
+    })
+
+    const api = new API('/campaigns')
+    await expect(api.delete('/123')).rejects.toThrow(APIError)
+  })
+
+  test('APIError carries HTTP status', async () => {
+    axios.delete.mockRejectedValue({
+      message: 'Delete failed',
+      response: { status: 403 }
+    })
+
+    const api = new API('/campaigns')
+    await expect(api.delete('/123')).rejects.toMatchObject({ status: 403 })
+  })
+})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import axios from 'axios'
import { API, APIError } from '../index'
jest.mock('axios')
afterEach(() => {
jest.clearAllMocks()
})
describe('API — URL construction', () => {
test('builds base URL with version', () => {
const api = new API('/campaigns', 'v1')
expect(api.baseUrl).toBe('/api/v1/campaigns')
})
test('builds base URL without version', () => {
const api = new API('/representatives')
expect(api.baseUrl).toBe('/api/representatives')
})
})
describe('API — get()', () => {
test('returns response.data on success', async () => {
const mockData = { id: 1, name: 'test' }
axios.get.mockResolvedValue({ data: mockData })
const api = new API('/campaigns')
const result = await api.get()
expect(result).toEqual(mockData)
})
test('throws APIError on failure', async () => {
axios.get.mockRejectedValue({
message: 'Not found',
response: { status: 404 }
})
const api = new API('/campaigns')
await expect(api.get()).rejects.toThrow(APIError)
})
test('APIError carries HTTP status', async () => {
axios.get.mockRejectedValue({
message: 'Not found',
response: { status: 404 }
})
const api = new API('/campaigns')
await expect(api.get()).rejects.toMatchObject({ status: 404 })
})
})
describe('API — post()', () => {
test('returns response.data on success', async () => {
const mockData = { letter: '<p>Hello</p>' }
axios.post.mockResolvedValue({ data: mockData })
const api = new API('/letter_templates', 'v1')
const result = await api.post('/render', { templateId: 1 })
expect(result).toEqual(mockData)
})
test('throws APIError on failure', async () => {
axios.post.mockRejectedValue({
message: 'Server error',
response: { status: 500 }
})
const api = new API('/letter_templates', 'v1')
await expect(api.post('/render', {})).rejects.toThrow(APIError)
})
test('APIError carries HTTP status', async () => {
axios.post.mockRejectedValue({
message: 'Server error',
response: { status: 500 }
})
const api = new API('/letter_templates', 'v1')
await expect(api.post('/render', {})).rejects.toMatchObject({ status: 500 })
})
})
import axios from 'axios'
import { API, APIError } from '../index'
jest.mock('axios')
afterEach(() => {
jest.clearAllMocks()
})
describe('API — URL construction', () => {
test('builds base URL with version', () => {
const api = new API('/campaigns', 'v1')
expect(api.baseUrl).toBe('/api/v1/campaigns')
})
test('builds base URL without version', () => {
const api = new API('/representatives')
expect(api.baseUrl).toBe('/api/representatives')
})
})
describe('API — get()', () => {
test('returns response.data on success', async () => {
const mockData = { id: 1, name: 'test' }
axios.get.mockResolvedValue({ data: mockData })
const api = new API('/campaigns')
const result = await api.get()
expect(result).toEqual(mockData)
})
test('throws APIError on failure', async () => {
axios.get.mockRejectedValue({
message: 'Not found',
response: { status: 404 }
})
const api = new API('/campaigns')
await expect(api.get()).rejects.toThrow(APIError)
})
test('APIError carries HTTP status', async () => {
axios.get.mockRejectedValue({
message: 'Not found',
response: { status: 404 }
})
const api = new API('/campaigns')
await expect(api.get()).rejects.toMatchObject({ status: 404 })
})
})
describe('API — post()', () => {
test('returns response.data on success', async () => {
const mockData = { letter: '<p>Hello</p>' }
axios.post.mockResolvedValue({ data: mockData })
const api = new API('/letter_templates', 'v1')
const result = await api.post('/render', { templateId: 1 })
expect(result).toEqual(mockData)
})
test('throws APIError on failure', async () => {
axios.post.mockRejectedValue({
message: 'Server error',
response: { status: 500 }
})
const api = new API('/letter_templates', 'v1')
await expect(api.post('/render', {})).rejects.toThrow(APIError)
})
test('APIError carries HTTP status', async () => {
axios.post.mockRejectedValue({
message: 'Server error',
response: { status: 500 }
})
const api = new API('/letter_templates', 'v1')
await expect(api.post('/render', {})).rejects.toMatchObject({ status: 500 })
})
})
describe('API — put()', () => {
test('returns response.data on success', async () => {
const mockData = { id: 1, updated: true }
axios.put.mockResolvedValue({ data: mockData })
const api = new API('/campaigns')
const result = await api.put('/123', { name: 'Updated' })
expect(result).toEqual(mockData)
})
test('throws APIError on failure', async () => {
axios.put.mockRejectedValue({
message: 'Update failed',
response: { status: 400 }
})
const api = new API('/campaigns')
await expect(api.put('/123', {})).rejects.toThrow(APIError)
})
test('APIError carries HTTP status', async () => {
axios.put.mockRejectedValue({
message: 'Update failed',
response: { status: 400 }
})
const api = new API('/campaigns')
await expect(api.put('/123', {})).rejects.toMatchObject({ status: 400 })
})
})
describe('API — delete()', () => {
test('returns response.data on success', async () => {
const mockData = { deleted: true }
axios.delete.mockResolvedValue({ data: mockData })
const api = new API('/campaigns')
const result = await api.delete('/123')
expect(result).toEqual(mockData)
})
test('throws APIError on failure', async () => {
axios.delete.mockRejectedValue({
message: 'Delete failed',
response: { status: 403 }
})
const api = new API('/campaigns')
await expect(api.delete('/123')).rejects.toThrow(APIError)
})
test('APIError carries HTTP status', async () => {
axios.delete.mockRejectedValue({
message: 'Delete failed',
response: { status: 403 }
})
const api = new API('/campaigns')
await expect(api.delete('/123')).rejects.toMatchObject({ status: 403 })
})
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/__tests__/api.test.js` around lines 1 - 82, Add unit tests for
API.put and API.delete mirroring the existing get/post suites: create tests that
(1) mock axios.put/axios.delete to resolve with { data: ... } and assert
API.put(path, body) and API.delete(path) return response.data, (2) mock
axios.put/axios.delete to reject with { message, response: { status } } and
assert the calls reject with APIError, and (3) assert the thrown APIError
carries the HTTP status (use rejects.toMatchObject({ status })) — place these
new describe/it blocks for API.put and API.delete after the current post() tests
and reuse the existing jest.mock('axios') setup and API constructor patterns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[front-end] Define a Common Interface for Front-end API Requests

1 participant