Valibot: A New Approach to Data Validation in JavaScript
I recently got to hang with the creator of Valibot, Fabian Hiller on a live stream. We discussed its history of the project and did some live coding with Valibot. Let’s dig in.
I recently got to hang with the creator of Valibot, Fabian Hiller on a live stream. We discussed its history of the project and did some live coding with Valibot. Let’s dig in.
Middleware exists in several frameworks like Next.js, Express, Hono and Fresh, and not just in JavaScript land. You can find it in frameworks like ASP.NET core in the .NET ecosystem, or Laravel in PHP. Since I mainly work in JavaScript and TypeScript these days, examples will be from frameworks in those ecosystems.
Middleware is something that happens in the middle of a user or some service interacting with a site or API call and happens at the framework level.
It runs before a page loads or an API endpoint is called, or more generally a route. There are many reasons why you might want to do that:
Let's dig in!
Authentication and authorization are two great candidates for guarding certain routes, although it’s still good to guard access to privied resources in API endpoints and pages. In this context, think of the middleware as the first line of defense.
In the OpenSauced application, when a user logs in and the path is /workspaces
we redirect them to their workspace.
{% raw %}
if (session?.user && req.nextUrl.pathname === "/workspaces") {
const data = await loadSession(req, session?.access_token);
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);
return NextResponse.redirect(`${workspaceUrl}`);
}
{% endraw %}
So what is a cookie?
A cookie is a way to set a piece of user-specific data. This could be a session ID for someone who is logged in to a site, or it could be some other user data. Note that the data in a cookie is typically not that large, but according to MDN, there is no size limit to the name or value of a cookie.
Cookies that are HTTP only can be accessed on the server-side, but for cookies that are not HTTP only, they can be accessed server-side and client-side. For example, you wouldn't want someone to tamper with your session ID on the client-side, so this type of cookie is set as HTTP only.
We recently shipped a new feature at OpenSauced, called Workspaces. You can read all about it in this great post from my co-worker Bekah (@BekahHW).
https://dev.to/opensauced/navigating-the-challenges-of-scaling-open-source-projects-11h2TLDR; a Workspace acts like a workspace in other software you may have used, like Notion. One thing required for this feature is when a user navigates to the /workspaces
URL, it has to load the last accessed workspace. If a user has never accessed a workspace before, it should default to their personal workspace. This is a perfect use case to leverage using a cookie.
When someone logs in, we check if they have a workspace ID cookie set. If they don’t, we grab their personal workspace ID, a type of workspace every user has.
The code for this was in the code snippet in the last section.
{% raw %}
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);
{% endraw %}
Let's take a peek into the getWorkspaceUrl
function.
{% raw %}
export function getWorkspaceUrl(cookies: RequestCookies, baseUrl: string, personalWorkspaceId: string) {
if (!cookies.has(WORKSPACE_ID_COOKIE_NAME)) {
cookies.set(WORKSPACE_ID_COOKIE_NAME, personalWorkspaceId);
}
// @ts-expect-error the cookie value will be defined
const workspaceId = cookies.get(WORKSPACE_ID_COOKIE_NAME).value;
return new URL(`/workspaces/${workspaceId}`, baseUrl);
}
{% endraw %}
If there is no workspace cookie set, we create a cookie and set its value to the user's personal workspace ID.
After that, we read the cookie, we build a URL with it and the user is redirected to the workspace.
The other piece of this that doesn't occur in middleware is when a user visits a valid workspace page they have access to, we set the workspace ID cookie. Next time they go to the /workspaces
link, the cookie will exist, and a URL using new URL()
will be used to redirect them to the last accessed workspace homepage.
The page will call the OpenSauced app's setCookie
function.
{% raw %}
export function setCookie({
response,
name,
value,
maxAge = 31536000,
sameSite = "Lax",
}: {
response: Response;
name: string;
value: string;
maxAge?: number;
sameSite?: SameSite;
}) {
response.setHeader(
"Set-Cookie",
`${name}=${value}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=${sameSite}; Secure`
);
}
{% endraw %}
Although cookies are their own thing, you have set them in a header.
As mentioned in the previous section, you set cookies via a header. So what is a header, more specifically, an HTTP header?
Headers are a set of key value pairs to let a browser know how to behave, for example, should a page be cached? It can also be custom key value pairs that your application or services might need. For example, when I worked at Netlify, for the CDN to work, there would be Netlify-specific headers that once inside the internal network would allow Netlify to do some magic.
If you go to my website, nickyt.co, and open the network panel in the dev tools of your browser of choice, you'll see some Netlify-specific headers.
I recently gave a talk on Fresh, a full-stack framework for Deno at Node Summit '24. The recording isn't up yet, but here's the slide deck and code from the demo for anyone interested.
In Fresh middleware, this is how you could set a header.
{% raw %}
export async function handler(
request: Request,
ctx: FreshContext<State>
) {
const response = await ctx.next();
response.headers.set("x-fresh", "true");
if (request.url.includes("joke-of-the-day")) {
response.headers.set("x-joke-page", "true");
}
if (request.url.includes("movie/")) {
response.headers.set("x-movie-page", "true");
}
return response;
}
{% endraw %}
In the above code snippet, we're checking to see if a specific route contains a certain string and if it does, we set a custom header, e.g.
{% raw %}
response.headers.set("x-joke-page", "true");
{% endraw %}
Page redirection allows you to have a URL go to another URL. You might do this for a couple of reasons. Maybe a bunch of links on your site changed, and you need to have them go to a new set of links, or you have a URL that needs to redirect to a user-specific page.
For non-trivial redirects like the workspaces redirect URL mentioned in one of the previous sections, middleware is a great place for handing redirects.
{% raw %}
if (session?.user && req.nextUrl.pathname === "/workspaces") {
const data = await loadSession(req, session?.access_token);
const workspaceUrl = getWorkspaceUrl(req.cookies, req.url, data.personal_workspace_id);
return NextResponse.redirect(`${workspaceUrl}`);
}
{% endraw %}
In this case, when someone in the OpenSauced application goes to /workspaces
we redirect them to a user-specific URL.
{% raw %}
return NextResponse.redirect(`${workspaceUrl}`);
{% endraw %}
Not a hard and fast rule, but if you have trivial redirects like redirect /old-blog-path/*
to /blog/*
, consider using your hosting platform's redirects instead of middleware.
You can also do URL rewrites. It's like a redirect, but the URL never changes. Frameworks like Next.js provide this out of the box in their configuration file, but for more complex handling, you may want to do it in middleware. So what is a URL rewrite? A rewrite will preserve the existing URL but will render content from another URL.
Here's a slightly modified example straight out of the Next.js middleware documentation:
{% raw %}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}
{% endraw %}
In the above snippet, all users have a /dashboard
page they go to, but every user's dashboard is different. In this case, the user will always see the page as /dashboard
but it loads the specific user's dashboard.
Here's the documentation for middleware of the mentioned frameworks:
Middleware is a great tool and if your framework of choice supports middleware (most do), I encourage you to read up on how to leverage it in that framework.
What use cases have you used middleware for? Please let me know in the comments.
Stay saucy peeps!
If you would like to know more about my work in open source, follow me on OpenSauced.
All the major browsers now support the <dialog%gt;
element. Why add this HTML element? User land code, code that developers write to fill in gaps of the browser, was doing similar things repeatedly, especially around focus trapping, and browser engines responded by adding this functionality directly in the browser.
What is focus trapping? It's a feature where you do not want focus outside a specific element, and that element typically contains focusable elements.
For example, a form in a modal to confirm an action: As a user uses the keyboard to navigate, they go to the next focusable element, e.g. a button.
If they reach the last focusable element in the modal, without focus trapping, the focus would go to the next focusable element in the document object model (DOM). With focus trapping, you go from the last focusable back to the first focusable element in the parent element.
In user land, popular packages like focus-trap have enabled developers to incorporate focus trapping.
With the dialog element, you get this for free, although there is a gotcha. If you add a dialog element to the page with the open attribute set, the dialog element will become visible on the page; however, focus trapping will not work as you'd expect in a modal.
From the API documentation:
Note: While you can toggle between the open and closed states of non-modal dialog boxes by toggling the presence of the open attribute, this approach is not recommended.
To get focus trapping working, the JavaScript API is required. You can display a modal on the screen by calling the HTMLDialogElement showModal method.
Note that you'll need to view this CodePen in full view because, for some reason, modal dialog focus trapping does not work in the CodePen editor view.
https://codepen.io/nickytonline/pen/NWJvbPeNot only do you get focus trapping, you also get modal close functionality that people have come to expect via the Escape key.
All of that is already amazing, but another common thing people were doing in user land was adding a background to block out users from interacting with the page. With the <dialog%gt;
element, we can add a ::backdrop
pseudo-element that does this for you. All you need to do is style it. In the CodePen above, uncomment out this code in the CSS panel to see this in action.
{% raw %}
dialog::backdrop {
background-color: purple;
opacity: 0.55;
filter: blur(100px);
}
{% endraw %}
The structure of a non-modal dialog element is the same as a modal dialog. The main difference is to show a non-modal dialog, you need to call the HTMLDialogElement show method.
With a non-modal dialog, the user is not blocked from navigating the rest of the page, i.e. no focus trapping, and the Escape key will not automatically close the dialog.
https://codepen.io/nickytonline/pen/ExMvNJwTo close a dialog or modal, we can use the HTMLDialogElement close method.
{% raw %}
const modal = document.querySelector("dialog");
// some button in the dialog that has a click event listener registered
modal.querySelector("button").addEventListener("click", () => {
modal.close();
});
{% endraw %}
The web platform keeps getting better. It's great to see pain points in user land that had user solutions come natively to browser land.
Stay saucy peeps!
If you would like to know more about my work in open source, follow me on OpenSauced.