prettier urls with nextjs routes - reactjs

I'm building out a new marketing site for my company using next.js, and I'm running into an issues with URLS. Essentially, I've built a custom API route to access data from our internal database, using Prisma:
getAllDealers.ts
import Cors from 'cors';
import { prisma } from 'lib/prisma';
import { NextApiResponse, NextApiRequest, NextApiHandler } from 'next';
const cors = Cors({
methods: ['GET', 'HEAD'],
});
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
const getDealers: NextApiHandler = async (req: NextApiRequest, res: NextApiResponse) => {
const { method } = req;
await runMiddleware(req, res, cors);
const dealers = await prisma.crm_dealers.findMany({
where: {
active: {
not: 0,
},
},
});
switch (method) {
case 'GET':
res.status(200).send({ dealers, method: method });
break;
case 'PUT':
res.status(500).json({ message: 'sorry, we only accept GET requests', method: method });
break;
default:
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${method} Not Allowed`);
}
};
export default getDealers;
And I've built a route to access individual dealers:
getSingleDealer.ts
import Cors from 'cors';
import { prisma } from 'lib/prisma';
import { NextApiResponse, NextApiRequest, NextApiHandler } from 'next';
const cors = Cors({
methods: ['GET', 'HEAD'],
});
function runMiddleware(req: NextApiRequest, res: NextApiResponse, fn: any) {
return new Promise((resolve, reject) => {
fn(req, res, (result: any) => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
const getDealerById: NextApiHandler = async (req: NextApiRequest, res: NextApiResponse) => {
await runMiddleware(req, res, cors);
const dealer = await prisma.crm_dealers.findUnique({
where: {
id: Number(req.query.id),
},
});
res.status(200).send({ dealer, method: req.method });
};
export default getDealerById;
I can use my getSingleDealer function in getServerSideProps like so:
export const getServerSideProps = async ({ params }: Params) => {
const { uid } = params;
const { dealer } = await getSingleDealer('api/dealer', uid);
return {
props: { dealer },
};
};
And this works just fine. What I need to do though is prettify my URLS. Right now, the way to access a singular dealer's page is dealers/1 with 1 being whatever the ID of the dealer is. I want to have that URL be a string, like dealers/sacramento-ca (that location is also served up in the API) while still accessing the API on the basis of the id so it's searching for an integer, rather than a string. Is that possible within next?

You'd handle the routing in your client with getServerSideProps similarly to what you are doing now. To do so, you need to configure your dynamic routing file or folder name to match your desired format.
Example folder structures are:
pages > dealers > [dealer].tsx = /dealers/sacramento-ca
pages > dealers > [location] > index.tsx = /dealers/sacramento-ca
export const getServerSideProps = async ({ params }: Params) => {
const { uid } = params;
const { dealer } = await getSingleDealer('api/dealer', uid);
if (!dealer ) {
return { notFound: true }
}
return {
props: {
...dealer,
location: 'sacramento-ca', // object key must match your dynamic [folder or file's name]
},
};
};
All dynamic URL parts must be included as a key in the return.
pages > dealers > [state] > index.tsx [city].tsx = /dealers/ca/sacramento
return {
props: {
...dealer,
city: 'sacramento',
state: 'ca',
},
};
Here is a article detailing what you will need to do. It's important to note that sometimes it's desirable to use a catch all route to simplify searching and deeply nested dynamic routes.

Related

Google OAuth components must be used within GoogleOAuthProvider

I want to build my next js project in which i am using
https://www.npmjs.com/package/#react-oauth/google
but when I build it i get the following :
this is layout.js and in _app.js I have all the components wrapped in GoogleOAuthProvider
import { GoogleLogin } from '#react-oauth/google';
import {FcGoogle} from "react-icons/Fc"
import { useGoogleLogin } from '#react-oauth/google';
export default function Layout({ children }) {
const client_id = ""
const responseGoogle = (response) => {
console.log(response);
}
CUTTED (NOT RELEVANT)
const login = useGoogleLogin({
onSuccess: codeResponse => {
const { code } = codeResponse;
console.log(codeResponse)
axios.post("http://localhost:8080/api/create-tokens", { code }).then(response => {
const { res, tokens } = response.data;
const refresh_token = tokens["refresh_token"];
const db = getFirestore(app)
updateDoc(doc(db, 'links', handle), {
refresh_token : refresh_token
})
updateDoc(doc(db, 'users', useruid), {
refresh_token : refresh_token
}).then(
CUTTED (NOT RELEVANT)
)
}).catch(err => {
console.log(err.message);
})
},
onError: errorResponse => console.log(errorResponse),
flow: "auth-code",
scope: "https://www.googleapis.com/auth/calendar"
});
return (
<>
CUTTED (NOT RELEVANT)
</>
)
}
Everything works perfect in dev mode but it does not want to build
I've faced this issue too. So I use 'GoogleLogin' instead of 'useGoogleLogin', then you can custom POST method on 'onSuccess' property.
import { GoogleLogin, GoogleOAuthenProvider} from '#react-oauth/google';
return(
<GoogleOAuthProvider clientId="YOUR CLIENT ID">
<GoogleLogin
onSuccess={handleLogin}
/>
</GoogleOAuthProvider>
The async function will be like...
const handleLogin = async = (credentialResponse) => {
var obj = jwt_decode(credentialResponse.credential);
var data = JSON.stringify(obj);
console.log(data);
const data = {your data to send to server};
const config = {
method: 'POST',
url: 'your backend server or endpoint',
headers: {},
data: data
}
await axios(config)
}
Spending whole day, this solve me out. Just want to share.
You have to wrap your application within GoogleOAuthProvider component. Please keep in mind that you will need your client ID for this.
import { GoogleOAuthProvider } from '#react-oauth/google';
<GoogleOAuthProvider clientId="<your_client_id>">
<SomeComponent />
...
<GoogleLoginButton onClick={handleGoogleLogin}/>
</GoogleOAuthProvider>;

How to logout automatically when session expires while using createAsyncThunk and axios (withcredential) option using react and redux toolkit?

I am trying to logout the user when the session expires after a certain period of time. I am using redux-toolkit with react for my API calls and, hence, using the createAsyncThunk middleware for doing so.
I have around 60 API calls made in maybe 20 slices throughout my application. Also, there is a async function for logout too that is fired up on the button click. Now the problem that I am facing is that if the session expires, I am not able to logout the user automatically. If I had to give him the message, then I had to take up that message from every api call and make sure that every screen of mine has a logic to notify the Unautherised message.
I did check a method called Polling that calls an API after a certain given time. And I believe that this is not a very efficient way to handle this problem.
**Here is a little code that will help you understand how my API calls are being made in the slices of my application. **
// Here is the custom created api that has axios and withcredentials value
import axios from "axios";
const api = axios.create({
baseURL:
process.env.NODE_ENV === "development" ? process.env.REACT_APP_BASEURL : "",
headers: {
"Content-Type": "application/json",
},
withCredentials: true,
});
export default api;
// My Logout Function!!
export const logoutUser = createAsyncThunk(
"userSlice/logoutUser",
async (thunkAPI) => {
try {
const response = await api.get("/api/admin/logout");
if (response.status === 200) {
return response.data;
} else {
return thunkAPI.rejectWithValue(response.data);
}
} catch (e) {
return thunkAPI.rejectWithValue(e.response.data);
}
}
);
I want to dispatch this function whenever there is a response status-code is 401 - Unauthorised. But I don't want to keep redundant code for all my other API calls calling this function. If there is a middleware that might help handle this, that would be great, or any solution will be fine.
// Rest of the APIs are called in this way.
..........
export const getStatus = createAsyncThunk(
"orgStat/getStatus",
async (thunkAPI) => {
try {
const response = await api.get("/api/admin/orgstat");
if (response.status === 200) {
return response.data;
} else {
return thunkAPI.rejectWithValue(response.data);
}
} catch (e) {
return thunkAPI.rejectWithValue(e.response.data);
}
}
);
const OrgStatusSlice = createSlice({
name: "orgStat",
initialState,
reducers: {
.......
},
extraReducers: {
[getStatus.pending]: (state) => {
state.isFetching = true;
},
[getStatus.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.isMessage = payload.message;
},
[getStatus.fulfilled]: (state, { payload }) => {
state.isFetching = false;
state.data = payload.data;
},
},
});
.......
If needed any more clearence please comment I will edit the post with the same.
Thank You!!
import axios from 'axios'
import errorParser from '../services/errorParser'
import toast from 'react-hot-toast'
import {BaseQueryFn} from '#reduxjs/toolkit/query'
import {baseQueryType} from './apiService/types/types'
import store from './store'
import {handleAuth} from './common/commonSlice'
import storageService from '#services/storageService'
// let controller = new AbortController()
export const axiosBaseQuery =
(
{baseUrl}: {baseUrl: string} = {baseUrl: ''}
): BaseQueryFn<baseQueryType, unknown, unknown> =>
async ({url, method, data, csrf, params}) => {
const API = axios.create({
baseURL: baseUrl,
})
API.interceptors.response.use(
(res) => {
if (
res.data?.responseCode === 1023 ||
res.data?.responseCode === 6023
) {
if(res.data?.responseCode === 1023){
console.log('session expired')
store.dispatch(handleSession(false))
return
}
console.log('Lopgged in somewhere else')
store.dispatch(handleSession(false))
storageService.clearStorage()
// store.dispatch(baseSliceWithTags.util.resetApiState())
return
// }, 1000)
}
return res
},
(error) => {
const expectedError =
error.response?.status >= 400 &&
error.response?.status < 500
if (!expectedError) {
if (error?.message !== 'canceled') {
toast.error('An unexpected error occurrred.')
}
}
if (error.response?.status === 401) {
// Storage.clearJWTToken();
// window.location.assign('/')
}
return Promise.reject(error)
}
)
try {
let headers = {}
if (csrf) headers = {...csrf}
const result = await API({
url: url,
method,
data,
headers,
params: params ? params : '',
baseURL: baseUrl,
// signal: controller.signal,
})
return {data: result.data}
} catch (axiosError) {
const err: any = axiosError
return {
error: {
status: errorParser.parseError(err.response?.status),
data: err.response?.data,
},
}
}
}
I am also using RTK with Axios. You can refer to the attached image.

How to handle error format in Redux-toolkit rtk-query graphql application

I'm developing an application based on redux-toolkit rtk-query and graphql.
I use graphql-codegen to generate the reducers starting from the graphql schema and everything working as expected.
Now i have a problem to handle errors. Has i understand redux-toolkit raise custom error with a specific format like this
{
name: "Error",
message: "System error",
stack:
'Error: System error: {"response":{"errors":[{"message":"System error","locations":[{"line":3,"column":3}],"path":["completaAttivita"],"extensions":{"errorCode":505,"classification":"VALIDATION","errorMessage":"Messaggio di errore","verboseErrorMessage":"it.cmrc.sid.backend.exception.CustomException: I riferimenti contabili non sono più validi","causedBy":"No Cause!"}}],"data":{"completaAttivita":null},"status":200,"headers":{"map":{"content-length":"398","content-type":"application/json"}}},"request":{"query":"\\n mutation completaAttivita($taskName: TipoAttivita, $taskId: String, $determinaId: BigInteger, $revisione: Boolean, $nota: NotaInputInput, $avanzaStatoDetermina: Boolean, $attribuzioniOrizzontali: AttribuzioniOrizzontaliInputInput, $firmaInput: FirmaInputInput, $roles: [String]) {\\n completaAttivita(\\n taskName: $taskName\\n taskId: $taskId\\n determinaId: $determinaId\\n revisione: $revisione\\n nota: $nota\\n avanzaStatoDetermina: $avanzaStatoDetermina\\n attribuzioniOrizzontali: $attribuzioniOrizzontali\\n firmaInput: $firmaInput\\n roles: $roles\\n ) {\\n id\\n }\\n}\\n ","variables":{"taskId":"24ac495b-46ca-42f4-9be2-fd92f0398114","determinaId":1342,"taskName":"firmaDirigente","firmaInput":{"username":"fdfs","password":"fdsf","otp":"fdsdf"}}}}\n at eval (webpack-internal:///../../node_modules/graphql-request/dist/index.js:354:31)\n at step (webpack-internal:///../../node_modules/graphql-request/dist/index.js:63:23)\n at Object.eval [as next] (webpack-internal:///../../node_modules/graphql-request/dist/index.js:44:53)\n at fulfilled (webpack-internal:///../../node_modules/graphql-request/dist/index.js:35:58)'
};
But my graphql endpoint return this
{
errors: [
{
message: "System error",
locations: [{ line: 3, column: 3 }],
path: ["completaAttivita"],
extensions: {
errorCode: 505,
classification: "VALIDATION",
errorMessage: "Messaggio di errore",
verboseErrorMessage:
"it.cmrc.sid.backend.exception.CustomException: Messaggio di errore",
causedBy: "No Cause!"
}
}
],
data: { completaAttivita: null }
};
Using rtk-query and the autogenerated client i have no access to the complete response from server.
And i need to extract the error messagge in the exceptions object.
From redix-toolkit documentation i understand that i need to catch the error and call rejectwithvalue() from a createAsyncThunk but i dont'undertand of to do that.
Here the base api object
import { createApi } from '#reduxjs/toolkit/query/react';
import { graphqlRequestBaseQuery } from './base-request';
import { GraphQLClient } from 'graphql-request';
import { getSession } from 'next-auth/react';
export const client = new GraphQLClient(
`${process.env.NEXT_PUBLIC_API_URL}/graphql`,
{
credentials: 'same-origin',
headers: {
Accept: 'application/json'
}
}
);
export const api = createApi({
baseQuery: graphqlRequestBaseQuery({
client,
prepareHeaders: async (headers, { getState }) => {
const session = await getSession();
if (session) {
headers.set('Authorization', `Bearer ${session?.access_token}`);
}
return headers;
}
}),
endpoints: () => ({}),
refetchOnMountOrArgChange: true
});
Thanks to #phry for merge my solution.
#rtk-query/graphql-request-base-query (version > 2.1.0) introduce a new configuration to handle errors format. Here a small explanation.
Typization
graphqlRequestBaseQuery<CustomErrorFormat>
Custom Error handler
...
customErrors: (props: ClientError) => CustomErrorFormat
...
Full example https://codesandbox.io/s/headless-microservice-uzujqb?file=/src/App.tsx
import { createApi } from '#reduxjs/toolkit/query/react';
import { graphqlRequestBaseQuery } from '#rtk-query/graphql-request-base-query';
import { ClientError, GraphQLClient } from 'graphql-request';
import { getSession } from 'next-auth/react';
export const client = new GraphQLClient(
`${process.env.NEXT_PUBLIC_API_URL}/graphql`,
{
credentials: 'same-origin',
headers: {
Accept: 'application/json'
}
}
);
export const api = createApi({
baseQuery: graphqlRequestBaseQuery<
Partial<ClientError & { errorCode: number }>
>({
client,
prepareHeaders: async (headers, { getState }) => {
const session = await getSession();
if (session) {
headers.set('Authorization', `Bearer ${session?.access_token}`);
}
return headers;
},
customErrors: ({ name, stack, response }) => {
const { errorMessage = '', errorCode = 500 } = response?.errors?.length
? response?.errors[0]?.extensions
: {};
return {
name,
message: errorMessage,
errorCode,
stack
};
}
}),
endpoints: () => ({}),
refetchOnMountOrArgChange: true
});
You can always write a wrapper around your baseQuery to reformat it:
const originalBaseQuery = graphqlRequestBaseQuery(...)
const wrappedBaseQuery = async (...args) => {
const result = await originalBaseQuery(...args);
if (result.error) {
// modify `result.error` here however you want
}
return result
}
It could also be necessary that you need to try..catch for that:
const originalBaseQuery = graphqlRequestBaseQuery(...)
const wrappedBaseQuery = async (...args) => {
try {
return await originalBaseQuery(...args);
} catch (e) {
// modify your error here
return { error: e.foo.bar }
}
}
I think this just slipped by when I was writing graphqlRequestBaseQuery and so far nobody has asked about it. If you have found a nice pattern of handling this, a pull request against graphqlRequestBaseQuery would also be very welcome.

Dynamic generated sitemap -there are only declared pages on sitemap

I'd like to generate dynamic url fo each slug, but there is an array only with pages which I declared: const pages = ["/", "/about", "/portfolio", "/blog"];
http://localhost:3000/api/my-sitemap. I've installed npm sitemap from https://www.npmjs.com/package/sitemap
my query in ../../lib/data.js
export const getBlogSlugs = async () => {
const endpoint =
"https://api-eu-central-gsagasgasxasasxsaxasxassaster";
const graphQLClient = new GraphQLClient(endpoint);
const query = gql`
{
posts {
slug
}
}
`;
return await graphQLClient.request(query);
};
pages/api/my-sitemap.js
import { getBlogSlugs } from "../../lib/data";
const { SitemapStream, streamToPromise } = require("sitemap");
const { Readable } = require("stream");
export const getStaticProps = async () => {
const { posts } = await getBlogSlugs();
return {
props: {
posts,
},
};
};
export default async (req, res, posts) => {
try {
const links = [];
posts?.map((slug) => {
links.push({
url: `/blog/${slug}`,
changefreq: "daily",
priority: 0.9,
});
});
// Add other pages
const pages = ["/", "/about", "/portfolio", "/blog"];
pages.map((url) => {
links.push({
url,
changefreq: "daily",
priority: 0.9,
});
});
// Create a stream to write to
const stream = new SitemapStream({
hostname: `https://${req.headers.host}`,
});
res.writeHead(200, {
"Content-Type": "application/xml",
});
const xmlString = await streamToPromise(
Readable.from(links).pipe(stream)
).then((data) => data.toString());
res.end(xmlString);
} catch (e) {
console.log(e);
res.send(JSON.stringify(e));
}
};
I added to my robots.txt in pudblic folder:
User-agent: *
Allow: /
Sitemap: http://localhost:3000/api/my-sitemap
What I got is only declared pages
localhost:3000/api/my-sitemap
I tried like this and doesn't work too:
export const getStaticProps = async () => {
const data = await getBlogSlugs();
return {
props: {
posts: data.posts,
},
};
};
export default async (req, res, posts) => {
try {
const links = [];
posts?.map((post) => {
links.push({
url: `/blog/${post.slug}`,
changefreq: "daily",
priority: 0.9,
});
});
You cannot use getStaticProps from an API route.
https://github.com/vercel/next.js/discussions/16068#discussioncomment-703870
You can fetch the data directly inside the API function.
Edit: In my app, I use the API route code below to fetch data from external server
import fetch from "isomorphic-unfetch";
export default async (req, res) => {
try {
const result = await fetch("YOUR_URL");
const posts = await result.json();
//use posts
});
} catch (e) {}
};
For GraphQl may be you can check the example given in vercel site
https://github.com/vercel/next.js/tree/canary/examples/api-routes-graphql

Pass value from server side to client side in Next JS using getInitialProps

I'm building a application using nextJS.
In server/index.ts, I have :
expressApp.get('/', (req: express.Request, res: express.Response) => {
const parsedUrl = parse(req.url, true);
const { query } = parsedUrl;
let username: string | undefined;
if (process.env.STAGE !== 'local') {
username = getUsername(req)?.toString();
}
return nextApp.render(req, res, '/', {...query, loggedInUser: username});
});
and in my home page (path is '/'), I auto direct to path '/pageone' by doing:
const Home = () => {
const router = useRouter();
useEffect(() => {
router.push('/pageone', undefined, { shallow: true });
}, []);
return <PageOne />;
};
Home.getInitialProps = async (ctx: NextPageContext): Promise<{ username : string | string[] }> => {
const query = ctx.query;
return {
username: query.loggedInUser? query.loggedInUser : 'testUser'
};
};
export default Home;
I need this username variable in every page I build, how can I pass it to every page(for example pageone)?
You can attach this username variable to the res.locals which this is it's purpose.
Then access the res.locals of the getInitialProps ctx.
// server.ts
// this express middleware will attach username to res.locals.username;
expressApp.use((req, res, next) => {
res.locals = res.locals || {};
if (process.env.STAGE !== 'local') {
res.locals.username = getUsername(req)?.toString();
}
next();
});
expressApp.get('/', (req: express.Request, res: express.Response) => {
const parsedUrl = parse(req.url, true);
const {query} = parsedUrl;
return nextApp.render(req, res, '/', query);
});
Then instead of passing this value to the client, as you did, you can redirect from server side within getInitialProps.
// Home.tsx
const Home = () => {
return <PageOne />;
};
Home.getInitialProps = async ({res}: NextPageContext): Promise<{ username : string | string[] }> => {
if(res.locals.username === 'bla') {
return res.redirect(301, '/pageone');
// -----------^ will redirect at server side.
}
};
export default Home;
I've made a module that might help. 👍
Next Coke API allows typed communication between client and server:
Server:
// define API methods
const routes = {
getName: async (body) => {
return "your name is " + body.name
}
}
// export types to the client
export type AppRoutes = typeof routes
// export nextCokeHandler
export default function handler(req, res) {
return nextCokeHandler(req, res, routes)
}
Client:
// define coke client
const { coke } = nextCokeClient<AppRoutes>()
// call API methods
coke.getName({ name: "John" }).then((res) => {
console.log(res)
})

Resources