Skip to content

Commit

Permalink
refactor(apiRequest): simplify logic and remove error handling depend…
Browse files Browse the repository at this point in the history
…ency

- Remove handleApiError dependency and its test
- Add TypeDoc documentation for apiRequest
- Add simple error handling inside apiRequest
  • Loading branch information
ishaan000 committed Jan 8, 2025
1 parent 6065006 commit 93fe6c9
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 202 deletions.
40 changes: 27 additions & 13 deletions src/hooks/apiRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { handleApiError } from '../utils/errorHandling';

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE';

export interface ApiOptions {
Expand All @@ -17,21 +15,37 @@ const defaultOptions: ApiOptions = {
credentials: 'include',
};

/**
* Makes an API request to the specified URL with the given options.
*
* @param {string} url - The endpoint to call, relative to `API_BASE_URL`.
* @param {ApiOptions} [options=defaultOptions] - Options for configuring the request,
* including HTTP method, headers, request body, and credentials. Defaults to `defaultOptions`.
* @returns {Promise<T>} A promise resolving to the parsed response data of type `T`.
* @throws {Error} Throws an error if the network response is not OK or if the request fails.
* The error contains the response body as a JSON string if available.
* @example
* // Example usage of a POST request
* const result = await apiRequest<{ message: string }>('/access/register', {
* method: 'POST',
* body: { email: '[email protected]', password: 'securePassword' },
* });
* console.log(result.message);
**/

export const apiRequest = async <T>(
url: string,
options: ApiOptions = defaultOptions
): Promise<T> => {
try {
const response = await fetch(`${API_BASE_URL}${url}`, {
...(options as RequestInit),
body: options.body && JSON.stringify(options.body),
});
const response = await fetch(`${API_BASE_URL}${url}`, {
...(options as RequestInit),
body: options.body && JSON.stringify(options.body),
});

return await handleApiError<T>(response);
} catch (error) {
if (error instanceof Error && 'flash' in error) {
throw error;
}
throw new Error('Unexpected API Error');
if (!response.ok) {
const errorResponse = await response.json();
throw new Error(JSON.stringify(errorResponse));
}

return response.json();
};
38 changes: 0 additions & 38 deletions src/utils/errorHandling.ts

This file was deleted.

78 changes: 35 additions & 43 deletions tests/app/hooks/apiRequest.test.tsx
Original file line number Diff line number Diff line change
@@ -1,121 +1,113 @@
import { ApiOptions, apiRequest } from '../../../src/hooks/apiRequest';
import { handleApiError } from '../../../src/utils/errorHandling';

global.fetch = jest.fn();

jest.mock('../../../src/utils/errorHandling', () => ({
handleApiError: jest.fn(),
}));

describe('apiRequest', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should use default options for a basic request', async () => {
it('should use default options for a basic GET request', async () => {
const mockResponse = { data: 'test data' };
(handleApiError as jest.Mock).mockResolvedValueOnce(mockResponse);
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse),
json: jest.fn().mockResolvedValueOnce(mockResponse),
});

await apiRequest('/test-endpoint');
const response = await apiRequest('/test-endpoint');

const fetchCalls = (global.fetch as jest.Mock).mock.calls;

expect(fetchCalls[0][0]).toBe(
`${process.env.NEXT_PUBLIC_BACKEND_URL}/test-endpoint`
);
expect(fetchCalls[0][1]).toMatchObject({
method: undefined,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
expect(response).toEqual(mockResponse);
});

it('should override default options with custom options', async () => {
const mockResponse = { data: 'test data' };
(handleApiError as jest.Mock).mockResolvedValueOnce(mockResponse);
const mockResponse = { data: 'custom data' };
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse),
json: jest.fn().mockResolvedValueOnce(mockResponse),
});

const customOptions: ApiOptions = {
method: 'POST',
headers: {
Authorization: 'Bearer token',
'Content-Type': 'application/json',
},
body: { name: 'test' },
credentials: 'include',
};

await apiRequest('/test-endpoint', customOptions);

const fetchCalls = (global.fetch as jest.Mock).mock.calls;

expect(fetchCalls[0][1]).toMatchObject(customOptions);
expect(fetchCalls[0][1]).toMatchObject({
method: 'POST',
headers: {
Authorization: 'Bearer token',
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ name: 'test' }),
});
});

it('should make a successful POST request', async () => {
const mockResponse = { data: 'created' };
const requestBody = { name: 'test' };
it('should make a successful POST request and return the correct response', async () => {
const mockResponse = { message: 'Success' };
const requestBody = { email: 'test@example.com', password: 'password123' };

(handleApiError as jest.Mock).mockResolvedValueOnce(mockResponse);
(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockResponse),
json: jest.fn().mockResolvedValueOnce(mockResponse),
});

const options: ApiOptions = {
method: 'POST',
body: requestBody,
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
};

const result = await apiRequest('/create', options);
const response = await apiRequest('/register', options);

expect(response).toEqual(mockResponse);

const fetchCalls = (global.fetch as jest.Mock).mock.calls;

expect(fetchCalls[0][1]).toMatchObject({
method: 'POST',
body: JSON.stringify(requestBody),
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
});
expect(result).toEqual(mockResponse);
});

it('should handle API errors', async () => {
const errorResponse = {
flash: {
error: ['Invalid request'],
},
};

const apiError = new Error('Invalid request');
Object.assign(apiError, {
flash: errorResponse.flash,
statusCode: 400,
});
it('should throw an error for non-OK responses', async () => {
const errorResponse = { message: 'Error occurred' };

(global.fetch as jest.Mock).mockResolvedValueOnce({
ok: false,
status: 400,
json: () => Promise.resolve(errorResponse),
json: jest.fn().mockResolvedValueOnce(errorResponse),
});

(handleApiError as jest.Mock).mockRejectedValueOnce(apiError);

await expect(apiRequest('/test-endpoint')).rejects.toThrow(
'Invalid request'
await expect(apiRequest('/error-endpoint')).rejects.toThrow(
JSON.stringify(errorResponse)
);
});

it('should handle network errors', async () => {
it('should handle network errors gracefully', async () => {
(global.fetch as jest.Mock).mockRejectedValueOnce(
new Error('Network error')
);

await expect(apiRequest('/test-endpoint')).rejects.toThrow(
'Unexpected API Error'
);
await expect(apiRequest('/test-endpoint')).rejects.toThrow('Network error');
});
});
108 changes: 0 additions & 108 deletions tests/utils/errorHandling.test.tsx

This file was deleted.

0 comments on commit 93fe6c9

Please sign in to comment.