This is a Draft Post
RFC: Next URL Validation
rigid UIsdrafted May 2025
The URL is Underrated
The URL has been called 'the OG state manager' and for good reason. But like all strings everywhere1Promise<>
and then awaiting, validating, casting that data in pages feels unnecessary and very awkward.
I think it's possible to make the URL typed, validated, and casted. And in doing-so, the URL becomes more enticing to utilize. I have some ideas.
The Ideas
Inspiration could come from nuqs and tanstack router2 which both deeply understand the importance of URLs and make it a rich environment to interact with. This would make the URL, which is currently a first-class untyped string interface into a fundamentally typed interface, with better dx ergonomics.
The idea here is to have more rigid and durable URLs, and be able to manipulate them through a more powerful interface. Through these ideas, it encourages people to move state into URLs, giving UIs more object permanence. Developers overuse in-memory client-side state (useState
). By making these APIs available natively, next.js can make the web become a more stable and rigid platform.
The big caveat here is that I don't know or understand the internals of next.js, nor am I an expert at complex typescript. I don't know if what I suggest here is possible or feasible, only that the general dx would be ideal for me (maybe not for others!). Let's take a look:
Preferably, validation should support Standard Schema so users can bring their own validation library. It's unclear if these should live next to pages or layouts, or otherwise. Later on I explain why this may be better moved to its own file (validation.ts
).
If a URL does not pass the validation, next should return a 400 (or similar) with a message indicating the URL params were invalid3
. This encourages developers to be more intentional with their URLs and provide strong safety for these generally untyped strings.Ideally, leafier components could look up the tree to pull out the validation4useSearchParams
to return a raw (typed) object (rather than the current URLSearchParams that requires .get
). This is one of those rare scenarios where leaning on web standards is actually more awkward dx.
A new (nuqs inspired) state getter/setter could be introduced to manage searchParams akin to useState (again relying on the underlying validation):
Links could be be fully typed and accept objects:
or
Shallow indicates client-only routing (vs. going to the server), again inspired by nuqs.
of course the same in router:
It might be worth taking this time to unify the naming across searchParams
and query
, doesn't really matter which one, but would be better if it was consistent. I know this probably comes from the location.
/href standard, but perhaps would be better to have consistency.
Developer Story
If you're still with me – let's walk through a developer story. We're creating a search page that data fetches on the server and then filters on the client.
So we start with a page:
and our client search:
This would be better if the query was in the URL, but fine enough. This is how 95% of React devs would solve it.
Let's say our data on the server is actually much larger than we can send on the client. Let's add the new URL validation and do our data processing on the server:
We could adjust these components to add suspense boundaries/transitions if we wanted.
and SearchInput:
Another Example - More Server Processing
Let's go back to our Client-Side example and add server-side pagination (how an average react dev would do it):
and
Alright this code has gotten pretty messy. What if we had all the primitives we designed above? Let's go back to our more complex example:
and ClientPagination:
And then when searching, make sure we reset page to 15
:That all feels a lot simpler and more rigid. There could be an individual useSearchParam
hook introduced like nuqs (akin to useQueryState
- perhaps a better name), though the bulkier one seems decent to me.
Validation.ts?
If validation was moved to its own isomorphic file (validation.ts
) we could run validation on the client and the server and get rid of { page: Math.max(1, page - 1) }
if we tried to switch it to page=>0
zod would reject it on the client and keep the page at 1
minimum. It could even be used in the buttons to run future-validation with some more magic:
Concerns+Thoughts
The url is a messy place. Affiliate tracking, referrals, etc. could break. So it probably is best to not validate unlisted search params (or to strip them). If it's possible to detect the default (or empty) search params, we could strip the URL leading to cleaner URLs (tanstack does this I believe).
Personally I find the having-to-await searchParams and params kind of annoying. I believe it's done because you want a heuristic to identify dynamic vs. static pages, but naively I don't know if that's the best heuristic. There are async
pages that are intended to be static: like a blog post6 . I'm sure there are more reasons than that alone, so if it's truly better then that's okay.
I think if these changes were to be implemented, you could get rid of searchParams
passed as props and have them be accessible in any server component (except for layouts? so sub-page maybe?) via await searchParams()
- which would return typed searchParams. And useSearchParams
for client components. Less prop drilling and less manual typing (that doesn't actually do anything).
I think a framework like next should embrace the URL as the powerful thing that it is, and I think providing a more natural dx surrounding it will lead to more people taking advantage of the URL (and thereby the server). Nothing here is specific to RSCs, but it does improve ergonomics. The URL is the thing that give our UIs permanence (sharing links, visiting via history) and developers should be encouraged to place (some) state there. When state is placed inside useState
calls, those states are lost when the tab closes.
Final hot take: Though it's a different situation, useState
reminds me of the trend useEffect
has gone, in that once people see different ways to store state (the url), we'll see it less abused and overused. I'm surprised how rarely I reach for useState
these days, choosing to stick things in the URL when appropriate. Server or client.
Thanks for reading. Happy to answer any questions.