Here is the solution that I have. Due to the legacy product we have all of our URLs in one big Slug model in rails that maps URLs to models.
EG
[
{ 'Product 1' => '/product-1'},
{ 'Category 1' => '/category-1'}
}
Next.js it does not support sending a component from getStaticProps
, so our [...slug].js
winds up including every possible component (probably 50% of the total site code) leading to large code chunks where 80% of the code is not necessary for the page being rendered.
The solution was to use a custom middleware, but being the first time I've done something like this, I would like a review of my implementation. the pseudo code is:
- Find url lookup in cache
- If doesn't exist in cache, query API
- If url lookup successful, rewrite to the appropriate page
- EG type Product =>
/p/${url}
, type Category =>/c/${url}
- EG type Product =>
- If URL lookup unsuccessful, do nothing and let Next move on.
// middleware.js
import { NextResponse } from 'next/server'
import cache from 'memory-cache'
// Seachs an api to see if a particulary url slug has a specific type set.
// Then appends the path passed to next with a subfolder so that url can only
// have a specific component to render instead of loading them all in one [...slug].js
// or having to dynamically import them. It also uses a memory cache to not have to make
// round trips to the api on every request.
export async function middleware(request) {
const cacheTimeout = 3600
const knownBadPaths = ['_next']
if (knownBadPaths.some(element => request.nextUrl.pathname.includes(element))) {
let apiRes = cache.get(request.nextUrl.pathname)
if (apiRes === null) {
const nextSlug = request.nextUrl.pathname.substring(1).replaceAll('/', ',')
const apiReq = await fetch(`${process.env.NEXT_PUBLIC_API_ROOT}/public/url_slugs/${nextSlug}`)
apiRes = await apiReq.json()
cache.put(request.nextUrl.pathname, apiRes, cacheTimeout)
}
if (apiRes.type === 'Category') {
return NextResponse.rewrite(new URL(`/c${request.nextUrl.pathname}`, request.url))
} else if (apiRes.type === 'Product') {
return NextResponse.rewrite(new URL(`/p${request.nextUrl.pathname}`, request.url))
} //... elseif for any other paths that need to be rewritten for specific types
}
}
export const config = {
matcher: '/:path*',
}
1 Answer 1
Feedback
For a first attempt at a middleware it looks decent. It is a bit on the long side and hopefully the suggestions below can help with that. It is great that strict type comparisons are used. Another good thing is that it appears that const
is used for any variable that does not need to be re-assigned.
Suggestions
Overwriting an existing variable
The variable apiRes
is used not only for the value returned from cache.get()
but also for the response from fetch()
. That function returns a Promise which resolves to a Response object. Then apiRes
is then assigned to the return value when apiRes.json()
is called. The json()
method returns a (regular, plain-old) JavaScript object. This could confuse someone reading the code - including your future self! Instead of re-assigning apiRes
one could assign the value to a variable with a name like slug
or slugData
.
Separation of Concerns
The comment below suggests that there will likely be more lines added depending on the value of the type property:
} //... elseif for any other paths that need to be rewritten for specific types
As the function grows it can lead to code that is not well organized. It would be wise to create smaller functions to handle individual aspects and then have the main function call those small functions. Those functions likely do not need to be exported with the module. Perhaps one function could handle the lookup of the URL in the cache and adding it if it doesn't exist. Another function could handle the rewrite based on the type property. Then when adding code to handle another type one wouldn't have to look through the main function to find the place to modify but instead the function for handling the rewrite based on the type property.
Error handling
When calling functions that could throw an exception like fetch()
it would be wise to catch exceptions and handle them. fetch()
can throw various exceptions. One can wrap the call to fetch()
in a try/catch
block, or call .catch()
after the call to fetch()
. At minimum perhaps checking myRes.ok
to see if it has a value of false
would be wise.
Repeated nested property
There are many places where request.nextUrl.pathname
is used. It could be stored in a variable with a shorter name so the lines that use it aren't so long. Destructuring assignment could be used to replace request
with the two properties used - i.e. nextUrl
and url
.
Constant casing
Per the common convention of many popular style guides (e.g. Google JS, airBnB) the constant cacheTimeout
could be named in UPPER_CASE
- e.g. CACHE_TIMEOUT
.
-
1\$\begingroup\$ Thank you. I will update my post with some refactored code. At this stage, it was more of a proof of concept. To see if I could dynamically route to a page component without a heavy performance hit. I still need to benchmark it. \$\endgroup\$Romuloux– Romuloux2022年08月16日 15:58:51 +00:00Commented Aug 16, 2022 at 15:58
Explore related questions
See similar questions with these tags.
apiRes = cache.get(request.nextUrl.pathname)
isnull
then is it possible that the call tofetch()
andapiRes = await apiReq.json()
thatapiRes
might still benull
or something that isn't an object? \$\endgroup\$apiRes
will never benull
. The server might return a404
,500
, etc. That is OK for this use case as this router only cares about pages that exist and routing them to the correct component. It will check at most every hour with the api to see if the errors have been resolved as the cache should timeout. \$\endgroup\$