Skip to content

Commit

Permalink
feat: easy api routing with loading, fetch hook and typings
Browse files Browse the repository at this point in the history
  • Loading branch information
OnlyNico43 committed Jul 6, 2024
1 parent 4bc647e commit 02f2e6a
Show file tree
Hide file tree
Showing 9 changed files with 446 additions and 0 deletions.
129 changes: 129 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
},
"dependencies": {
"@nextui-org/react": "^2.4.2",
"axios": "^1.7.2",
"framer-motion": "^11.2.12",
"i18next": "^23.11.5",
"i18next-http-backend": "^2.5.2",
"i18next-resources-to-backend": "^1.2.1",
"next": "14.2.4",
"next-connect": "^1.0.0",
"next-i18n-router": "^5.5.0",
"react": "^18",
"react-dom": "^18",
Expand Down
42 changes: 42 additions & 0 deletions src/app/api/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { serverSideRequest } from '@/src/services/api-services/server-side';
import { AxiosError } from 'axios';
import { NextApiRequest, NextApiResponse } from 'next';
import { createRouter } from 'next-connect';

const router = createRouter<NextApiRequest, NextApiResponse>();

const routerHandler = router.handler({
onError: (err, _: NextApiRequest, res: NextApiResponse) => {
console.error(err);
res.status(500).send({ statusCode: 500, message: 'Internal Server Error' });
},
onNoMatch: (_: NextApiRequest, res: NextApiResponse) => {
res.status(405).send({ statusCode: 405, message: 'Method not allowed' });
},
});

router.post(async (req: NextApiRequest, res: NextApiResponse) => {
try {
const response = await serverSideRequest(req, req.body);
res.setHeader('Set-Cookie', response.headers['set-cookie'] ? response.headers['set-cookie'] : []);
res.status(200).send(response.data);
} catch (err) {
if (!(err instanceof AxiosError)) throw err;
const msg = {
request: {
url: err.config?.url,
},
status: err.response?.status,
statusText: err.response?.statusText,
body: JSON.stringify(err.response?.data),
};

console.error(msg);

const statusCode = err.response?.status ?? 500;

res.status(statusCode).send(err.response?.data ?? { statusCode, message: 'Internal Server Error' });
}
});

export default routerHandler;
7 changes: 7 additions & 0 deletions src/enums/error-codes.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export enum ErrorCode {
API_ERROR = 'API_ERROR',
NOT_FOUND = 'NOT_FOUND',
NOT_ALLOWED = 'NOT_ALLOWED',
WRONG_CREDENTIALS = 'WRONG_CREDENTIALS',
ALREADY_EXISTS = 'ALREADY_EXISTS',
}
50 changes: 50 additions & 0 deletions src/hooks/useFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { ErrorCode } from '../enums/error-codes.enum';
import { clientSideRequest } from '../services/api-services/client-side';
import { APIContext, APIOperation } from '../services/api-services/common';

type useFetchType = {
isLoading: boolean;
setIsLoading: Dispatch<SetStateAction<boolean>>;
fetchDataWithLoadingTimeout: <T extends APIOperation, R = APIContext[T]['responseType']>(
config: Omit<APIContext[T], 'responseType'> & {
op: T;
},
) => Promise<
| {
success: true;
data: R;
}
| { success: false; errorCode: ErrorCode }
>;
};

const useFetch = (loadingTimeout = 100): useFetchType => {
const [isLoading, setIsLoading] = useState<boolean>(false);
let timeout: ReturnType<typeof setTimeout>;

useEffect(() => {
return () => clearInterval(timeout);
});

const fetchDataWithLoadingTimeout = async <T extends APIOperation, R = APIContext[T]['responseType']>(
config: Omit<APIContext[T], 'responseType'> & { op: T },
): Promise<{ success: true; data: R } | { success: false; errorCode: ErrorCode }> => {
timeout = setTimeout(() => {
setIsLoading(true);
}, loadingTimeout);

const res = await clientSideRequest(config);

clearInterval(timeout);
setIsLoading(false);

// eslint-disable-next-line
// @ts-ignore
return res;
};

return { isLoading, setIsLoading, fetchDataWithLoadingTimeout };
};

export default useFetch;
35 changes: 35 additions & 0 deletions src/services/api-services/client-side.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ErrorCode } from '@/src/enums/error-codes.enum';
import axios, { AxiosError } from 'axios';
import { APIContext, APIOperation } from './common';

const clientSideRequest = async <T extends APIOperation, R = APIContext[T]['responseType']>(
options: Omit<APIContext[T], 'responseType'> & { op: T },
): Promise<{ success: true; data: R } | { success: false; errorCode: ErrorCode }> => {
if (typeof window === 'undefined') throw new Error('Request can only be performed on the client side');

try {
const { data } = await axios.post<R>(
'/api',
{
...options,
},
{ withCredentials: true },
);

return { success: true, data };
} catch (err) {
if (!(err instanceof AxiosError)) return { success: false, errorCode: ErrorCode.API_ERROR };

if (typeof err.response?.data !== 'object') return { success: false, errorCode: ErrorCode.API_ERROR };

if (typeof err.response.data.error !== 'string') return { success: false, errorCode: ErrorCode.API_ERROR };

const errorCode = err.response.data.error;

if (!Object.values(ErrorCode).includes(errorCode)) return { success: false, errorCode: ErrorCode.API_ERROR };

return { success: false, errorCode };
}
};

export { clientSideRequest };
Loading

0 comments on commit 02f2e6a

Please sign in to comment.