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.
As web developers, we know that most of the create, read update, and delete (CRUD) actions we perform on the web are typically (hopefully?) done using an HTML form.
HTML Forms are cool because they have plenty of built-in features.
For example, they have native form validation and access to all the inputs in a form, and at some point, because you need to submit the form, there is a mechanism to do that as well. You can use a button, <button>submit</button>
or an input of type submit, <input type="submit" />
, although the latter isn't used as much these days in new sites, from what I've seen.
Here is a simple form to exhibit this.
https://codepen.io/nickytonline/pen/JjVOarXIf you fill out the form and click submit, the form will submit and add a paragraph with dark green text client-side that says, "Form submitted".
There are other things in the simple form, like form validation via the required attribute on inputs, but that's not what we're here to discuss.
What we want to touch on is that the form was able to handle the submit event because it had a submit button associated with it, which was defined in HTML within the form element.
Note: you can press enter in fields to submit a form, but again, not what we're here to discuss.
This brings us to a new feature that I was working on for OpenSauced for a few months, workspaces. I encourage you to create your own, but for now, let's get back to the business of forms.
Here's our beautiful workspaces settings page that I implemented.
Recently, there were styling changes, which is what you see above.
https://github.com/open-sauced/app/pull/2982Everything looked great, and I had tested it.
Narrator: he thought he had tested it, and we shipped things to production.
Once things went live, I decided to do some smoke tests, which I usually do. I went over to the beautiful workspace settings I had worked on, made some changes in the settings, and then clicked Update Workspace button. Hmm, no toast message saying the settings were updated. I checked the browser dev tools to see if there were any JavaScript errors. Nothing related to the updates. And then it dawned on me. The submit button was outside the form, and I just broke some key functionality in the app.
Side note, but luckily thanks to Netlify's deployment rollback feature, I was able to revert to the previous production deployment within about a minute of the workspace settings page being broken 😅
So how did I fix it? We needed this new styling to fix several other issues related to z-indexes and layout.
For some context, the OpenSauced application is a Next.js site, so React, but I decided to put on my old school HTML hat and remembered that form elements can be associated to a form via a form
attribute. What you need to do is give the form an id
attribute, and the form element that you want to associate the form to needs to have a form
attribute whose value is the value of the id
attribute for the form.
Here's another simple form demonstrating a simplified version of my fix.
https://codepen.io/nickytonline/pen/XWQzPOXI encourage you to remove the form
attribute from the button in the above CodePen to see the issue I caused.
Here's the fix I rolled out to production.
https://github.com/open-sauced/app/pull/3003Learning a framework is great, and I'm a big proponent of just building something, but as you continue on in your career, it's great to start getting some fundamentals into the mix.
Also, this is a perfect example of why using semantic HTML is important! It definitely helped me get out of jam! 😅
Stay saucy peeps!
If you would like to know more about my work in open source, follow me on OpenSauced.
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.
People definitely have opinions about Tailwind. There are staunch supporters and staunch haters, but I really don't want to get into all that. Head on over to Twitter if you want to waste some time.
If you're pretty well versed with Tailwind, this article might not be for you, but who knows? Read on and maybe you'll learn something.
I'm coming in with what, I think, is a fresh perspective. I'm using Tailwind for the first time professionally. Furthermore, I don't consider myself a CSS expert, but I think I have pretty solid CSS skills.
I mention all this, to convey a sentiment, I've seen many people exhibit. You're using Tailwind because you don't understand CSS. I do understand CSS.
So the first thing that I've seen when people say when they do not like Tailwind, is that it's not CSS, or it's inline CSS. This is completely false, even coming in as a newbie to Tailwind, all Tailwind is, at the end of the day, once it's compiled, is CSS utility classes.
So let's look at some comparisons between Tailwind and "real" CSS. I'm going to put the vanilla CSS in a style
tag, but you could also put it in a .css
file and link it in the head
of your HTML or however your application bundles CSS. This is just for the sake of comparison.
Vanilla CSS
{% raw %}
<style>
.my-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.my-list li {
border-width: 1px;
}
</style>
<ul class="my-list">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
{% endraw %}
Tailwind
{% raw %}
<ul class="flex flex-col gap-6">
<li class="border">Item 1</li>
<li class="border">Item 2</li>
<li class="border">Item 3</li>
</ul>
{% endraw %}
So the first thing someone might say is that Tailwind is repeating the border
CSS class on a list item, <li>
, instead of using a selector that can target the li
DOM elements. This is true, but Tailwind allows you to create the equivalent of .my-list li
. You can do the following:
{% raw %}
<ul class="flex flex-col gap-6 [&_li]:border">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
{% endraw %}
This is probably where someone might say, "Well, now you're just writing inline CSS." This is also false. It will generate a CSS rule based on the [&_li]:border
CSS class name. It will compile it to literal CSS that will generate an equivalent CSS rule comparable to the CSS rule for the .mylist li
selector.
In fact, this is what it compiles to. I've formatted it since it gets minified.
{% raw %}
.\[\&_li\]\:border li {
border-width: 1px;
}
{% endraw %}
You could make an argument that the "real" version looks nicer, but this isn't a strong argument, and you have CSS source maps if you open the browser dev tools.
I'll say it here and repeat it again later. Tailwind is a utility-first CSS framework. It's not inline CSS.
If you want to see an example of this in production grade code, check out a recent pull request (PR) of mine to the OpenSauced app repository.
https://github.com/open-sauced/app/pull/2524What about something more complex like pseudo-elements? Let's take the ::before pseudo-element for a spin.
Vanilla CSS
{% raw %}
<style>
.pizza-time::before {
content: attr(data-inset-label);
}
</style>
<p data-inset-label="🍕" class="pizza-time">OpenSauced is awesome!</p>
{% endraw %}
Tailwind
{% raw %}
<p data-inset-label="🍕" class="before:content-[attr(data-inset-label)]">
OpenSauced is awesome!
</p>
{% endraw %}
Here's what it generates as CSS when Tailwind compiles that CSS class.
{% raw %}
.before\:content-\[attr\(data-inset-label\)\]:before{
--tw-content:attr(data-inset-label);
content:var(--tw-content)
}
{% endraw %}
You could complain that that is one hell of a bloated CSS class name, but again, I don't think this is a colossal deal.
If you want to see an example of this in production grade code, check out a recent PR of mine to the OpenSauced app repository.
https://github.com/open-sauced/app/pull/2552If you're looking to add animations, Tailwind ships with plenty of useful animations and CSS classes to leverage them.
Need a custom animation? You can do that as well. I won't go into it here, but here's a great post about writing custom animations in Tailwind.
You've got all these cool animations, but what if someone has specified prefers-reduced-motion? Tailwind can handle that for you as long as you prefix your animation with motion-safe:
, e.g.
{% raw %}
<p class="motion-safe:animate-spin">Spinning text</p>
{% endraw %}
There's other useful Tailwind classes for accessibility, like sr-only, which will remain in the page, but only be visible to screen readers.
I think something that would be interesting to add to the Tailwind a11y story is using Tatiana Mac's (@tatianamac) approach of taking a no-motion-first approach to animations.
I'm all for components, and I'm a big fan of JSX. Tailwind pairs nicely with components, but I do think that it's still good to have some base styles defined, even if you are using components.
For example, a base font size and colour, focus state styles, headings etc. This is what I ended up doing in the OpenSauced app repository.
Tailwind CSS on its own is not like bootstrap. It's just CSS utility classes, whereas bootstrap is UI components and CSS.
I've never used it, but maybe you could fall into this trap with Tailwind UI.
Like many things, there are tradeoffs. I think the biggest one is learning the Tailwind CSS classes and naming conventions for building them, but I think the benefits outweigh this. And to be honest, once you start writing the classes frequently, the naming convention just sticks in your head.
And if you have some super complex CSS, for whatever reason, Tailwind can't handle, there's nothing wrong with adding some custom CSS.
I literally only started using Tailwind September 18th of 2023 when I started at OpenSauced.
Tailwind has made me super productive while building out OpenSauced, and I've used it in some other projects since then.
Remember, Tailwind is a utility-first CSS framework. It's not inline CSS.
I encourage you to give Tailwind a go. They have outstanding documentation and great IDE support to help you along the way.
If you give it a go and say it's not for me, that's OK. Use what makes you the most productive.
Stay saucy peeps!
If you would like to know more about my work in open source, follow me on OpenSauced.