Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature/jacoblee23/crud-routes: Handle HTTP request types #34

Merged
merged 7 commits into from
Feb 5, 2024
192 changes: 192 additions & 0 deletions frontend/src/api/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
/**
* Based on the TSE onboarding API client implementation:
* https://github.com/TritonSE/onboarding/blob/main/frontend/src/api/requests.ts
*/

/**
* Custom type definition for the HTTP methods handled by this module.
*/
type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

/**
* Throws an error if the status code of the HTTP response indicates an error. If an HTTP error was
* raised, throws an error.
*
* @param response A `Response` object returned by `fetch()`
* @throws An `Error` object if the response status was not successful (2xx) or a redirect (3xx)
*/
async function assertOK(response: Response): Promise<void> {
if (response.ok) {
return;
}

let message = `${response.status} ${response.statusText}`;

const text = await response.text();
if (text) {
message += `: ${text}`;
}

throw new Error(message);
}

/**
* Wrapper for the `fetch()` function.
*
* @param method The HTTP method (see `Method`)
* @param url The URL to request from
* @param body The request body (or undefined, if none)
* @param headers The request headers
* @returns A `Response` object returned by `fetch()`
*/
async function fetchRequest(
method: Method,
url: string,
body: unknown,
headers: Record<string, string>,
): Promise<Response> {
const newHeaders = { ...headers };
if (body !== undefined) {
newHeaders["Content-Type"] = "application/json";
}

const response = await fetch(url, {
method,
headers: newHeaders,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
await assertOK(response);

return response;
}

/**
* Sends a GET request to the indicated API URL.
*
* @param url The URL to request from
* @param headers The request headers
* @returns A `Response` object returned by `fetch()`
*/
export async function GET(url: string, headers: Record<string, string> = {}): Promise<Response> {
return await fetchRequest("GET", url, undefined, headers);
}

/**
* Sends a POST request with the provided request body to the indicated API URL.
*
* @param url The URL to request from
* @param body The request body (or undefined, if none)
* @param headers The request headers
* @returns A `Response` object returned by `fetch()`
*/
export async function POST(
url: string,
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("POST", url, body, headers);
}

/**
* Sends a PUT request with the provided request body to the indicated API URL.
*
* @param url The URL to request from
* @param body The request body (or undefined, if none)
* @param headers The request headers
* @returns A `Response` object returned by `fetch()`
*/
export async function PUT(
url: string,
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("PUT", url, body, headers);
}

/**
* Sends a PATCH request with the provided request body to the indicated API URL.
*
* @param url The URL to request from
* @param body The request body (or undefined, if none)
* @param headers The request headers
* @returns A `Response` object returned by `fetch()`
*/
export async function PATCH(
url: string,
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("PATCH", url, body, headers);
}

/**
* Sends a DELETE request with the provided request body to the indicated API URL.
*
* @param url The URL to request from
* @param body The request body (or undefined, if none)
* @param headers The request headers
* @returns A `Response` object returned by `fetch()`
*/
export async function DELETE(
url: string,
body: unknown,
headers: Record<string, string> = {},
): Promise<Response> {
return await fetchRequest("DELETE", url, body, headers);
}

/**
* Utility type for the result of a successful API result. See `APIResult`.
*/
export type APIData<T> = { success: true; data: T };
/**
* Utility type for the result of an unsuccessful API result. See `APIResult`.
*/
export type APIError = { success: false; error: string };
/**
* Utility type for the result of an API request. API client functions should
* return an object of this type, which allows implementations of the functions
* to perform more straightforward exception-checking without requiring
* extensive `try`-`catch` hadnling, making use of TypeScript's type narrowing
* feature.
*
* By checking the value of the `success` field, it can be quickly determined
* whether the `data` field (containing an actual API response) or the `error`
* field (containing an error message) should be accessed.
*
* Recommended usage:
*
* ```
* if (result.success) {
* console.log(result.data);
* } else {
* console.error(result.error);
* }
* ```
*/
export type APIResult<T> = APIData<T> | APIError;

/**
* Helper function for API client functions for consistent error handling.
*
* Recommended usage:
*
* ```
* try {
* ...
* } catch (error) {
* return handleAPIError(error);
* }
* ```
*
* @param error The error thrown by a lower-level API function
* @returns An `APIError` object with a message from the given error
*/
export function handleAPIError(error: unknown): APIError {
if (error instanceof Error) {
return { success: false, error: error.message };
} else if (typeof error === "string") {
return { success: false, error };
}
return { success: false, error: `Unknown error; ${String(error)}` };
}
Loading