Skip to content

Handling Forms in Svelte

This article was written over 18 months ago and may contain information that is out of date. Some content may be relevant but please refer to the relevant official documentation or available resources for the latest information.

Handling Forms in Svelte

If you're building an app or website, it's very likely that at some point you need to add a form to it.

In this post, we'll dive into how to handle forms in Svelte, starting from a simple approach. Then, we'll start adding complexity to it, and finally, we'll have a reusable solution.

Starting with a simple form

Let's build a sign-in form with two fields: email and password.

We'll begin by creating a new Svelte project (I prefer vite).

npm init @vitejs/app

✔ Project name: · forms-in-svelte
✔ Select a framework: · svelte
✔ Select a variant: · svelte-ts

cd forms-in-svelte
pnpm install //use the package manager you prefer
pnpm run dev

NOTE: At the moment of writing there's a bug with Svelte TypeScript projects. If you get an error, add "type":"module" in your package.json file.

Now that we have our application ready, we'll start by removing the content of App.svelte and create a sign-in form.

We should also delete the Counter component created by default.

<!-- App.svelte -->
<main>
  <form>
    <div>
        <label for="name">Email</label>
        <input
          type="text"
          id="email"
          name="email"
          value=""
        />
    </div>
    <div>
      <label for="name">Password</label>
      <input
        type="password"
        id="password"
        name="password"
        value=""
      />
  </div>
    <button type="submit">Submit</button>
  </form>
</main>

<style>
   * {
      box-sizing: border-box;
    }
    form {
      display: flex;
      flex-direction: column;
      width: 300px;
    }

    form > div{
      display: flex;
      justify-content: space-between;
    }

    form > div + * {
      margin-top: 10px;
    }
</style>
svelte-forms-img-1

Handling the submit event

Currently, our form does nothing. We need to handle the submit event on the form. To do that, we must add a handler.

To handle a submit event, we need to add to the form element the on:submit handler.

For now, we will only parse the form, and print it in the console.

<!-- App.svelte -->
<script lang="ts">
  function onSubmit(e) {
    const formData = new FormData(e.target);

    const data = {};
    for (let field of formData) {
      const [key, value] = field;
      data[key] = value;
    }
    console.log(data)
  }
</script>

<main>
  <form on:submit|preventDefault={onSubmit}>
   <!-- ... -->
  </form>
</main>
<!-- ... -->
svelte-forms-img-2

Note that we used the event modifier |preventDefault. This is equivalent to adding the Event preventDefault method in the handler.

Adding validation

Now that our form "works", we can add more features to it. For example, we may want to add some validation when the form is submitted. We will make both inputs required. If the form is invalid, we will print an error in the console. Otherwise, we will print the form data.

<!-- App.svelte -->
<script lang="ts">
  function isFormValid(data: {[fieldName: string]: any}): boolean {
    if(!isRequiredFieldValid(data.email)){
      return false
    }

    if(!isRequiredFieldValid(data.password)){
      return false
    }
    return true
  }

  function isRequiredFieldValid(value){
    return value != null && value !== ""
  }

  function onSubmit(e) {
    const formData = new FormData(e.target);

    const data: any = {};
    for (let field of formData) {
      const [key, value] = field;
      data[key] = value;
    }
    if(isFormValid(data)){
      console.log(data)
    } else {
      console.log("Invalid Form")
    }
  }
</script>
<!-- ... -->
svelte-forms-gif-1

Displaying errors

Now, our form is running a simple validation on submit, but we are missing feedback for the user. It's time to display some errors in case something fails.

<!-- App.svelte -->
<script lang="ts">
  let errors: { [inputName: string]: any } = {};

  function isFormValid(data: { [inputName: string]: any }): boolean {
    return !Object.keys(errors).some((inputName) =>
      Object.keys(errors[inputName]).some(
        (errorName) => errors[inputName][errorName],
      ),
    );
  }

  function validateForm(data: { [inputName: string]: any }):void {
    if (!isRequiredFieldValid(data.email)) {
      errors['email'] = { ...errors['email'], required: true };
    } else {
      errors['email'] = { ...errors['email'], required: false };
    }

    if (!isRequiredFieldValid(data.password)) {
      errors['password'] = { ...errors['password'], required: true };
    } else {
      errors['password'] = { ...errors['password'], required: false };
    }
  }

  function isRequiredFieldValid(value) {
    return value != null && value !== '';
  }

  function onSubmit(e) {
    const formData = new FormData(e.target);

    const data: any = {};
    for (let field of formData) {
      const [key, value] = field;
      data[key] = value;
    }

    validateForm(data);

    if (isFormValid(data)) {
      console.log(data);
    } else {
      console.log('Invalid Form');
    }
  }
</script>

<main>
  <form on:submit|preventDefault={onSubmit}>
    <div>
      <label for="name">Email</label>
      <input type="text" id="email" name="email" value="" />
      {#if errors.email && errors.email.required}
        <p class="error-message">Email is required</p>
      {/if}
    </div>
    <div>
      <label for="name">Password</label>
      <input type="password" id="password" name="password" value="" />
      {#if errors.password && errors.password.required}
        <p class="error-message">Password is required</p>
      {/if}
    </div>
    <button type="submit">Submit</button>
  </form>
</main>

<style>
  /* ... */

  form > div {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }

  /* ... */

  .error-message {
    color: tomato;
    flex: 0 0 100%;
    margin: 0 2px;
    font-size: 0.8em;
  }
</style>

First, I created an error object to keep track of each field, and its errors. I created it as an empty object as I expect it to be populated when verification happens.

Three functions will help validation:

  • isRequiredFieldValid checks if a given value is null, undefined, or an empty string.
  • validateForm sets errors to true or false for each form input. In this example, we are checking if both required fields are valid.
  • isFormValidwill check if there's an error in the form.

In the template, error messages are added below each form input, with a message to be displayed if the error is present and true.

Finally, some CSS was added for styling the form.

The result looks something like this:

svelte-forms-gif-2

Preparing for reusability

We have a working form right now, but it would be better if somehow we could make our form reusable.

The pattern will be repeated for each input we add. It would be nice if we could make it configurable, or if we could easily add more errors and validators without repeating ourselves.

Let's move the validators to a new file, and add more information to the response.

// Validators.ts
export interface ValidatorResult {
  [validatorName: string]: {
    error: boolean;
    message?: string;
  };
}

export type ValidatorFn = (value: any) => ValidatorResult;

function required(value: any): ValidatorResult {
  if (value === '' || value == null) {
    return { required: { error: true, message: 'Field is required' } };
  }
  return { required: { error: false } };
}

export const Validators = {
  required,
};

Previously, we had the isRequiredFieldValid method returning just a boolean. Now it returns an object with the error name required, error status, and a message, but can be extended with more information if required.

We now need to make use of this new Validator in our App component.

<!-- App.svelte -->
<script lang="ts">
  import { Validators } from './lib/Validators';
  import type { ValidatorFn, ValidatorResult } from './lib/Validators';

  let errors: { [inputName: string]: ValidatorResult } = {};

  let form: {
    [inputName: string]: {
      validators: ValidatorFn[];
    };
  } = {
    email: {
      validators: [Validators.required],
    },
    password: {
      validators: [Validators.required],
    },
  };

  function isFormValid(): boolean {
    return !Object.values(errors).some((field) =>
      Object.values(field).some((errorObject) => errorObject.error),
    );
  }

  function validateForm(data: { [inputName: string]: any }): void {
    Object.keys(data).forEach((field) => validateField(field, data[field]));
  }

  function validateField(field, value) {
    form[field]?.validators &&
      form[field].validators.forEach((fn) => {
        const error = fn(value);
        errors[field] = { ...errors[field], ...error };
      });
  }

  function onSubmit(e) {
    const formData = new FormData(e.target);

    const data: any = {};
    for (let field of formData) {
      const [key, value] = field;
      data[key] = value;
    }

    validateForm(data);

    if (isFormValid()) {
      console.log(data);
    } else {
      console.log('Invalid Form');
    }
  }
</script>

<main>
  <form on:submit|preventDefault={onSubmit}>
    <div>
      <label for="name">Email</label>
      <input type="text" id="email" name="email" value="" />
      {#if errors?.email?.required?.error}
        <p class="error-message">Email is required</p>
      {/if}
    </div>
    <div>
      <label for="name">Password</label>
      <input type="password" id="password" name="password" value="" />
      {#if errors?.password?.required?.error}
        <p class="error-message">Password is required</p>
      {/if}
    </div>
    <button type="submit">Submit</button>
  </form>
</main>

<!-- ... -->

First, I've created a form object that contains the different input names with a list of the validators that should be checked for each of them.

I added the required validator for both of them.

Then we will run all validators in the form object where the key matches the name of the input element.

The template has been updated too to handle the validator response object.

With this small refactoring, we've preserved our previous behavior, but opened the door to expand our form.

More validators

Let's add another validator to our password input.

We'll check if the password has a minimum length of 6 characters.

We'll start by creating a new validator. The best way is to use a higher-order function to set up the length, and return our validator function from it.

export interface ValidatorResult {
  [validatorName: string]: {[key:string]:any} & {
    error: boolean;
    message?: string;
  };
}

// ...

function minLength(number) {
  return function (value): ValidatorResult {
    if (value == null || value.length < number) {
      return {
        minLength: {
          error: true,
          value: number, 
          message: `Field minimum length is ${number}`,
        },
      };
    }
    return { minLength: { error: false } };
  };
}

export const Validators = {
  required,
  minLength
};

Now, we need to add it to our form configuration object, and handle the error in the template. We are using the error default message.

<!-- App.svelte -->
<script lang="ts">
// ...
  let form: {
    [inputName: string]: {
      validators: ValidatorFn[];
    };
  } = {
    email: {
      validators: [Validators.required],
    },
    password: {
      validators: [Validators.required, Validators.minLength(6)],
    },
  };

 //...
</script>

<main>
  <form on:submit|preventDefault={onSubmit}>
   <!-- ... -->
    <div>
      <label for="name">Password</label>
      <input type="password" id="password" name="password" value="" />
      {#if errors?.password?.required?.error}
        <p class="error-message">Password is required</p>
      {/if}
	  {#if errors?.password?.minLength?.error}
        <p class="error-message">{errors.password.minLength.message}</p>
      {/if}
    </div>
    <button type="submit">Submit</button>
  </form>
</main>

<!-- ... -->
svelte-forms-img-3

Handling other form events

I want to add another feature to our form. I want to validate each field separately on blur.

We can use an event handler with one of each input element to do it.

<!-- App.svelte -->
<script lang="ts">
  //...

  function onBlur(e){
    validateField(e.target.name, e.target.value)
  }

</script>

<main>
  <form on:submit|preventDefault={onSubmit}>
    <!-- ... -->
      <input type="text" id="email" name="email" value="" on:blur={onBlur} />
    <!-- ... -->
      <input type="password" id="password" name="password" value="" on:blur={onBlur} />
       <!-- ... -->
  </form>
</main>

<!-- ... -->

We just added an on:blur event handler, and onBlur method to take care of everything. Now, everytime an input loses focus it will be validated.

svelte-forms-gif-3

Our form is working as expected. However, I'd like to move it to a new component.

Creating a reusable form component

Let's create a new component Form.svelte, and move most of the form logic into it.

The form configuration should remain in the App component, and be passed into the new Form component.

<!-- Form.svelte -->
<script lang="ts">
  import { setContext } from 'svelte';
  import type { ValidatorFn, ValidatorResult } from './Validators';
  import { createEventDispatcher } from 'svelte';
  import { writable } from 'svelte/store';

  export let form: {
    [inputName: string]: {
      validators: ValidatorFn[];
    };
  } = {};

  let formEl;

  const dispatch = createEventDispatcher();
  let errors = writable({});

  function onBlur(e) {
    validateField(e.target.name, e.target.value);
  }

  function isFormValid(): boolean {
    return !Object.values($errors).some((field) =>
      Object.values(field).some(
        (errorObject: ValidatorResult) => errorObject.error,
      ),
    );
  }

  function validateField(field, value) {
    form[field]?.validators &&
      form[field].validators.forEach((fn) => {
        const error = fn(value);
        errors.update((e) => {
          e[field] = { ...e[field], ...error };
          return e;
        });
      });
  }

  function validateForm(data: { [inputName: string]: any }): void {
    Object.keys(data).forEach((field) => validateField(field, data[field]));
  }

  function onSubmit(e) {
    const formData = new FormData(e.target);

    const data: any = {};
    for (let field of formData) {
      const [key, value] = field;
      data[key] = value;
    }
    validateForm(data);

    return dispatch('submit', { valid: isFormValid(), data });
  }

  export function reset() {
    formEl.reset();
  }

  setContext('form', { errors, onBlur });
</script>

<form on:submit|preventDefault={onSubmit} bind:this={formEl}>
  <slot />
</form>

<style>
  form {
    display: flex;
    flex-direction: column;
    width: 300px;
  }

  :global(form > div) {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
  }

  :global(form > div + *) {
    margin-top: 10px;
  }
</style>

The template is very simple. A form element with a slot.

We are also binding the form element, and the on:submit event.

There's only one input: form, which will be in charge of passing data from the parent component.

The element's binding will be used to expose the native reset method from the form element. And the event binding will be used to perform validations, and emit a new submit event to be handled by the parent component.

One important thing to notice is that, to communicate the slotted elements with the Form component, we will use the Context API. There's one caveat though, context is not reactive. But, we can make it reactive by making its content a store.

Currently, we will only need to pass two things to the slotted content, the onBlur method, and the errors object. Because we expect the error object to be changing, I rewrote it as a writable store. Note that everything that previously used the errors object will have to use this new store.

To keep the parent component simple (the one containing the form), the form content will be wrapped in new components that will make use of the context API to get the data from the Form component.

Because I want always to have a label for any given field, I'll make it part of the Input component, and because I want to add validation on blur, I'll get the onBlur method exposed by the Form component through the context API.

<!-- Input.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  export let type = 'text';
  export let label;
  export let name;
  const { onBlur } = getContext('form');
</script>

<label for={name}>{label}</label>
<input {name} {type} on:blur={onBlur} />

We are closer to getting the same behavior as before, but we are missing the error components. We can wrap all that functionality in a single component, and get the error information through the context API.

<!-- Error.svelte -->
  import { getContext } from 'svelte';
  const { errors } = getContext('form');
  export let message = null;
  export let fieldName;
  export let errorKey;
</script>

{#if $errors?.[fieldName]?.[errorKey]?.error}
  <p class="error-message">{message || $errors[fieldName][errorKey].message}</p>
{/if}

<style>
  .error-message {
    color: tomato;
    flex: 0 0 100%;
    margin: 0 2px;
    font-size: 0.8em;
  }
</style>

We are now reading the value from the errors store to decide if the message should be shown or not.

We are also using a default message from the validator in case no message is provided.

Now that all the pieces are ready, we can update our App component.

<!-- App.svelte -->
<script lang="ts">
  import { Validators } from './lib/Validators';
  import Form from './lib/Form.svelte';
  import Input from './lib/Input.svelte';
  import Error from './lib/Error.svelte';

  let formEl;
  let form = {
    email: {
      validators: [Validators.required],
    },
    password: {
      validators: [Validators.required, Validators.minLength(6)],
    },
  };

  function onSubmit(e) {
    if (e?.detail?.valid) {
      console.log(e.detail.data);
      setTimeout(() => formEl.reset(), 1000)
    } else {
      console.log('Invalid Form');
    }
  }
</script>

<main>
  <Form {form} on:submit={onSubmit} bind:this={formEl}>
    <div>
      <Input label="Email" name="email" />
      <Error
        fieldName="email"
        errorKey="required"
        message="Email is required"
      />
    </div>
    <div>
      <Input label="Password" name="password" />
      <Error
        fieldName="password"
        errorKey="required"
        message="Password is required"
      />
      <Error fieldName="password" errorKey="minLength" />
    </div>
    <button type="submit">Submit</button>
  </Form>
</main>

<style>
  * {
    box-sizing: border-box;
  }
</style>

Our app component now only needs to set up the validators, handle the submit event, and can reset the form if needed.

Extending the form component

Our form component is now exposing just a few things emitted on the submit event, or through the context API, for the slotted components.

Similar to what we've done with the input field, we must extend the functionality for select, radio buttons, checkboxes, or any other form control that we want.

For example, this is how a select component may look like.

<!-- Select.svelte -->
<script lang="ts">
  import { getContext } from 'svelte';
  export let label;
  export let name;
  const { onBlur } = getContext('form');
</script>

<label for={name}>{label}</label>
<select {name} on:blur={onBlur}>
  <slot />
</select>

And this is how it could be used:

<!-- App.svelte -->
<script lang="ts">
  import { Validators } from './lib/Validators';
  import Form from './lib/Form.svelte';
  import Input from './lib/Input.svelte';
  import Error from './lib/Error.svelte';
  import Select from './lib/Select.svelte';

  let form = {
    name: {
      validators: [Validators.required],
    },
    food: {
      validators: [Validators.required],
    },
  };

  let formEl;

  function onSubmit(e) {
    if (e?.detail?.valid) {
      console.log(e.detail.data);
      setTimeout(() => formEl.reset(), 1000)
    } else {
      console.log('Invalid Form');
    }
  }
</script>

<main>
  <Form {form} on:submit={onSubmit} bind:this={formEl}>
    <div>
      <Input label="Name" name="name" />
      <Error
        fieldName="name"
        errorKey="required"
        message="Name is required"
      />
    </div>
    <div>
      <Select label="Favorite food" name="food">
        <option value="chocolate">Chocolate</option>
        <option value="pizza">Pizza</option>
      </Select>
    </div>
    <button type="submit">Submit</button>
  </Form>
</main>
<!-- ... -->

Available Libraries

There are plenty of available libraries for handling forms in Svelte. You can check some of them in this list.

Final thoughts

I hope this step-by-step guide to handling forms helps you find your own solution to handling them, or gives you a better understanding of how other libraries might be working.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.

You might also like

D1 SQLite: Writing queries with the D1 Client API cover image

D1 SQLite: Writing queries with the D1 Client API

Writing queries with the D1 Client API In the previous post we defined our database schema, got up and running with migrations, and loaded some seed data into our database. In this post we will be working with our new database and seed data. If you want to participate, make sure to follow the steps in the first post. We’ve been taking a minimal approach so far by using only wrangler and sql scripts for our workflow. The D1 Client API has a small surface area. Thanks to the power of SQL, we will have everything we need to construct all types of queries. Before we start writing our queries, let's touch on some important concepts. Prepared statements and parameter binding This is the first section of the docs and it highlights two different ways to write our SQL statements using the client API: prepared and static statements. Best practice is to use prepared statements because they are more performant and prevent SQL injection attacks. So we will write our queries using prepared statements. We need to use parameter binding to build our queries with prepared statements. This is pretty straightforward and there are two variations. By default we add ? ’s to our statement to represent a value to be filled in. The bind method will bind the parameters to each question mark by their index. The first ? is tied to the first parameter in bind, 2nd, etc. I would stick with this most of the time to avoid any confusion. ` I like this second method less as it feels like something I can imagine messing up very innocently. You can add a number directly after a question mark to indicate which number parameter it should be bound to. In this exampl, we reverse the previous binding. ` Reusing prepared statements If we take the first example above and not bind any values we have a statement that can be reused: ` Querying For the purposes of this post we will just build example queries by writing them out directly in our Worker fetch handler. If you are building an app I would recommend building functions or some other abstraction around your queries. select queries Let's write our first query against our data set to get our feet wet. Here’s the initial worker code and a query for all authors: ` We pass our SQL statement into prepare and use the all method to get all the rows. Notice that we are able to pass our types to a generic parameter in all. This allows us to get a fully typed response from our query. We can run our worker with npm run dev and access it at http://localhost:8787 by default. We’ll keep this simple workflow of writing queries and passing them as a json response for inspection in the browser. Opening the page we get our author results. joins Not using an ORM means we have full control over our own destiny. Like anything else though, this has tradeoffs. Let’s look at a query to fetch the list of posts that includes author and tags information. ` Let’s walk through each part of the query and highlight some pros and cons. ` * The query selects all columns from the posts table. * It also selects the name column from the authors table and renames it to author_name. * It aggregates the name column from the tags table into a JSON array. If there are no tags, it returns an empty JSON array. This aggregated result is renamed to tags. ` * The query starts by selecting data from the posts table. * It then joins the authors table to include author information for each post, matching posts to authors using the author_id column in posts and the id column in authors. * Next, it left joins the posts_tags table to include tag associations for each post, ensuring that all posts are included even if they have no tags. * Next, it left joins the tags table to include tag names, matching tags to posts using the tag_id column in posts_tags and the id column in tags. * Finally, group the results by the post id so that all rows with the same post id are combined in a single row SQL provides a lot of power to query our data in interesting ways. JOIN ’s will typically be more performant than performing additional queries.You could just as easily write a simpler version of this query that uses subqueries to fetch post tags and join all the data by hand with JavaScript. This is the nice thing about writing SQL, you’re free to fetch and handle your data how you please. Our results should look similar to this: ` This brings us to our next topic. Marshaling / coercing result data A couple of things we notice about the format of the result data our query provides: Rows are flat. We join the author directly onto the post and prefix its column names with author. ` Using an ORM we might get the data back as a child object: ` Another thing is that our tags data is a JSON string and not a JavaScript array. This means that we will need to parse it ourselves. ` This isn’t the end of the world but it is some more work on our end to coerce the result data into the format that we actually want. This problem is handled in most ORM’s and is their main selling point in my opinion. insert / update / delete Next, let’s write a function that will add a new post to our database. ` There’s a few queries involved in our create post function: * first we create the new post * next we run through the tags and either create or return an existing tag * finally, we add entries to our post_tags join table to associate our new post with the tags assigned We can test our new function by providing post content in query params on our index page and formatting them for our function. ` I gave it a run like this: http://localhost:8787authorId=1&tags=Food%2CReview&title=A+review+of+my+favorite+Italian+restaurant&content=I+got+the+sausage+orchette+and+it+was+amazing.+I+wish+that+instead+of+baby+broccoli+they+used+rapini.+Otherwise+it+was+a+perfect+dish+and+the+vibes+were+great And got a new post with the id 11. UPDATE and DELETE operations are pretty similar to what we’ve seen so far. Most complexity in your queries will be similar to what we’ve seen in the posts query where we want to JOIN or GROUP BY data in various ways. To update the post we can write a query that looks like this: ` COALESCE acts similarly to if we had written a ?? b in JavaScript. If the binded value that we provide is null it will fall back to the default. We can delete our new post with a simple DELETE query: ` Transactions / Batching One thing to note with D1 is that I don’t think the traditional style of SQLite transactions are supported. You can use the db.batch API to achieve similar functionality though. According to the docs: Batched statements are SQL transactions ↗. If a statement in the sequence fails, then an error is returned for that specific statement, and it aborts or rolls back the entire sequence. ` Summary In this post, we've taken a hands-on approach to exploring the D1 Client API, starting with defining our database schema and loading seed data. We then dove into writing queries, covering the basics of prepared statements and parameter binding, before moving on to more complex topics like joins and transactions. We saw how to construct and execute queries to fetch data from our database, including how to handle relationships between tables and marshal result data into a usable format. We also touched on inserting, updating, and deleting data, and how to use transactions to ensure data consistency. By working through these examples, we've gained a solid understanding of how to use the D1 Client API to interact with our database and build robust, data-driven applications....

Introduction to Zod for Data Validation cover image

Introduction to Zod for Data Validation

As web developers, we're often working with data from external sources like APIs we don't control or user inputs submitted to our backends. We can't always rely on this data to take the form we expect, and we can encounter unexpected errors when it deviates from expectations. But with the Zod library, we can define what our data ought to look like and parse the incoming data against those defined schemas. This lets us work with that data confidently, or to quickly throw an error when it isn't correct. Why use Zod? TypeScript is great for letting us define the shape of our data in our code. It helps us write more correct code the first time around by warning us if we are doing something we shouldn't. But TypeScript can't do everything for us. For example, we can define a variable as a string or a number, but we can't say "a string that starts with user_id_ and is 20 characters long" or "an integer between 1 and 5". There are limits to how much TypeScript can narrow down our data for us. Also, TypeScript is a tool for us developers. When we compile our code, our types are not available to the vanilla JavaScript. JavaScript can't validate that the data we actually use in our code matches what we thought we'd get when we wrote our TypeScript types unless you're willing to manually write code to perform those checks. This is where we can reach for a tool like Zod. With Zod, we can write data schemas. These schemas, in the simplest scenarios, look very much like TypeScript types. But we can do more with Zod than we can with TypeScript alone. Zod schemas let us create additional rules for data parsing and validation. A 20-character string that starts with user_id_? It's z.string().startsWith('user_id_').length(20). An integer between 1 and 5 inclusive? It's z.number().int().gte(1).lte(5). Zod's primitives give us many extra functions to be more specific about *exactly* what data we expect. Unlike TypeScript, Zod schemas aren't removed on compilation to JavaScript—we still have access to them! If our app receives some data, we can verify that it matches the expected shape by passing it to your Zod schema's parse function. You'll either get back your data in exactly the shape you defined in your schema, or Zod will give you an error to tell you what was wrong. Zod schemas aren't a replacement for TypeScript; rather, they are an excellent complement. Once we've defined our Zod schema, it's simple to derive a TypeScript type from it and to use that type as we normally would. But when we really need to be sure our data conforms to the schema, we can always parse the data with our schema for that extra confidence. Defining Data Schemas Zod schemas are the variables that define our expectations for the shape of our data, validate those expectations, and transform the data if necessary to match our desired shape. It's easy to start with simple schemas, and to add complexity as required. Zod provides different functions that represent data structures and related validation options, which can be combined to create larger schemas. In many cases, you'll probably be building a schema for a data object with properties of some primitive type. For example, here's a schema that would validate a JavaScript object representing an order for a pizza: ` Zod provides a number of primitives for defining schemas that line up with JavaScript primitives: string, number, bigint, boolean, date, symbol, undefined, and null. It also includes primitives void, any, unknown, and never for additional typing information. In addition to basic primitives, Zod can define object, array, and other native data structure schemas, as well as schemas for data structures not natively part of JavaScript like tuple and enum. The documentation contains considerable detail on the available data structures and how to use them. Parsing and Validating Data with Schemas With Zod schemas, you're not only telling your program what data should look like; you're also creating the tools to easily verify that the incoming data matches the schema definitions. This is where Zod really shines, as it greatly simplifies the process of validating data like user inputs or third party API responses. Let's say you're writing a website form to register new users. At a minimum, you'll need to make sure the new user's email address is a valid email address. For a password, we'll ask for something at least 8 characters long and including one letter, one number, and one special character. (Yes, this is not really the best way to write strong passwords; but for the sake of showing off how Zod works, we're going with it.) We'll also ask the user to confirm their password by typing it twice. First, let's create a Zod schema to model these inputs: ` So far, this schema is pretty basic. It's only making sure that whatever the user types as an email is an email, and it's checking that the password is at least 8 characters long. But it is *not* checking if password and confirmPassword match, nor checking for the complexity requirements. Let's enhance our schema to fix that! ` By adding refine with a custom validation function, we have been able to verify that the passwords match. If they don't, parsing will give us an error to let us know that the data was invalid. We can also chain refine functions to add checks for our password complexity rules: ` Here we've chained multiple refine functions. You could alternatively use superRefine, which gives you even more fine grained control. Now that we've built out our schema and added refinements for extra validation, we can parse some user inputs. Let's see two test cases: one that's bound to fail, and one that will succeed. ` There are two main ways we can use our schema to validate our data: parse and safeParse. The main difference is that parse will throw an error if validation fails, while safeParse will return an object with a success property of either true or false, and either a data property with your parsed data or an error property with the details of a ZodError explaining why the parsing failed. In the case of our example data, userInput2 will parse just fine and return the data for you to use. But userInput1 will create a ZodError listing all of the ways it has failed validation. ` ` We can use these error messages to communicate to the user how they need to fix their form inputs if validation fails. Each error in the list describes the validation failure and gives us a human readable message to go with it. You'll notice that the validation errors for checking for a valid email and for checking password length have a lot of details, but we've got three items at the end of the error list that don't really tell us anything useful: just a custom error of Invalid input. The first is from our refine checking if the passwords match, and the second two are from our refine functions checking for password complexity (numbers and special characters). Let's modify our refine functions so that these errors are useful! We'll add our own error parameters to customize the message we get back and the path to the data that failed validation. ` Now, our error messages from failures in refine are informative! You can figure out which form fields aren't validating from the path, and then display the messages next to form fields to let the user know how to remedy the error. ` By giving our refine checks a custom path and message, we can make better use of the returned errors. In this case, we can highlight specific problem form fields for the user and give them the message about what is wrong. Integrating with TypeScript Integrating Zod with TypeScript is very easy. Using z.infer&lt;typeof YourSchema> will allow you to avoid writing extra TypeScript types that merely reflect the intent of your Zod schemas. You can create a type from any Zod schema like so: ` Using a TypeScript type derived from a Zod schema does *not* give you any extra level of data validation at the type level beyond what TypeScript is capable of. If you create a type from z.string.min(3).max(20), the TypeScript type will still just be string. And when compiled to JavaScript, even that will be gone! That's why you still need to use parse/safeParse on incoming data to validate it before proceeding as if it really does match your requirements. A common pattern with inferring types from Zod schemas is to use the same name for both. Because the schema is a variable, there's no name conflict if the type uses the same name. However, I find that this can lead to confusing situations when trying to import one or the other—my personal preference is to name the Zod schema with Schema at the end to make it clear which is which. Conclusion Zod is an excellent tool for easily and confidently asserting that the data you're working with is exactly the sort of data you were expecting. It gives us the ability to assert at runtime that we've got what we wanted, and allows us to then craft strategies to handle what happens if that data is wrong. Combined with the ability to infer TypeScript types from Zod schemas, it lets us write and run more reliable code with greater confidence....

Exploring Open Props and its Capabilities cover image

Exploring Open Props and its Capabilities

Exploring Open Props and its Capabilities With its intuitive approach and versatile features, Open Props empowers you to create stunning designs easily. It has the perfect balance between simplicity and power. Whether you're a seasoned developer or just starting, Open Props makes styling websites a breeze. Let's explore how Open Props can help your web development workflow. What is Open Props Open Props is a CSS library that packs a set of CSS variables for quickly creating consistent components using “Sub-Atomic” Styles. These web design tokens are crafted to help you get great-looking results from the start using consistent naming conventions and providing lots of possibilities out-of-the-box. At the same time, it's customizable and can be gradually adopted. Installing open props There are many ways to get started with open props, each with its advantages. The library can be imported from a CDN or installed using npm. You can import all or specific parts of it, and for greater control of what's bundled or not, you can use PostCSS to include only the variables you used. From Zero to Open Props Let's start with the simplest way to test and use open props. I'll create a simple HTML file with some structure, and we'll start from there. Create an index.html file. ` Edit the content of your HTML file. In this example, we’ll create a landing page containing a few parts: a hero section, a section for describing the features of a service, a section for the different pricing options available and finally a section with a call to action. We’ll start just declaring the document structure. Next we’ll add some styles and finally we’ll switch to using open props variables. ` To serve our page, we could just open the file, but I prefer to use serve, which is much more versatile. To see the contents of our file, let's serve our content. ` This command will start serving our site on port 3000. Our site will look something like this: Open-props core does not contain any CSS reset. After all, it’s just a set of CSS variables. This is a good start regarding the document structure. Adding open props via CDN Let's add open-props to our project. To get started, add: ` This import will make the library's props available for us to use. This is a set of CSS variables. It contains variables for fonts, colors, sizes, and many more. Here is an excerpt of the content of the imported file: ` The :where pseudo-class wraps all the CSS variables declarations, giving them the least specificity. That means you can always override them with ease. This imported file is all you need to start using open props. It will provide a sensible set of variables that give you some constraints in terms of what values you can use, a palette of colors, etc. Because this is just CSS, you can opt-out by not using the variables provided. I like these constraints because they can help with consistency and allow me to focus on other things. At the same time, you can extend this by creating your own CSS variables or just using any value whenever you want to do something different or if the exact value you want is not there. We should include some styles to add a visual hierarchy to our document. Working with CSS variables Let's create a new file to hold our styles. ` And add some styles to it. We will be setting a size hierarchy to headings, using open props font-size variables. Additionally, gaps and margins will use the size variables. ` We can explore these variables further using open-props’ documentation. It's simple to navigate (single page), and consistent naming makes it easy to learn them. Trying different values sometimes involves changing the number at the end of the variable name. For example: font-size-X, where X ranges from 0 to 8 (plus an additional 00 value). Mapped to font-sizes from 0.5rem up to 3.5rem. If you find your font is too small, you can add 1 to it, until you find the right size. Colors range from 0-12: –red-0 is the lightest one (rgb(255, 245, 245)) while –red-12 is the darkest (rgb(125, 26, 26)). There are similar ranges for many properties like font weight, size (useful for padding and margins), line height and shadows, to name a few. Explore and find what best fits your needs. Now, we need to include these styles on our page. ` Our page looks better now. We could keep adding more styles, but we'll take a shortcut and add some defaults with Open Props' built in normalized CSS file. Besides the core of open props (that contains the variables) there’s an optional normalization file that we can use. Let's tweak our recently added styles.css file a bit. Let’s remove the rules for headings. Our resulting css will now look like this. ` And add a new import from open-props. ` Open props provides a normalization file for our CSS, which we have included. This will establish a nice-looking baseline for our styles. Additionally, it will handle light/dark mode based on your preferences. I have dark mode set and the result already looks a lot better. Some font styles and sizes have been set, and much more. More CSS Variables Let's add more styles to our page to explore the variables further. I'd like to give the pricing options a card style. Open Props has a section on borders and shadows that we can use for this purpose. I would also like to add a hover effect to these cards. Also, regarding spacing, I want to add more margins and padding when appropriate. ` With so little CSS added and using many open props variables for sizes, borders, shadows, and easing curves, we can quickly have a better-looking site. Optimizing when using the CDN Open props is a pretty light package; however, using the CDN will add more CSS than you'll probably use. Importing individual parts of these props according to their utility is possible. For example, import just the gradients. ` Or even a subset of colors ` These are some options to reduce the size of your app if using the CDN. Open Props with NPM Open Props is framework agnostic. I want my site to use Vite. Vite is used by many frameworks nowadays and is perfect to show you the next examples and optimizations. ` Let's add a script to our package.json file to start our development server. ` Now, we can start our application on port 5173 (default) by running the following command: ` Your application should be the same as before, but we will change how we import open props. Stop the application and remove the open-props and normalize imports from index.html. Now in your terminal install the open-props package from npm. ` Once installed, import the props and the normalization files at the beginning of your styles.css file. ` Restart your development server, and you should see the same results. Optimizing when using NPM Let's analyze the size of our package. 34.4 kb seems a bit much, but note that this is uncompressed. When compressed with gzip, it's closer to 9 kb. Similar to what we did when using the CDN, we can add individual sections of the package. For example in our CSS file we could import open-props/animations or open-props/sizes. If this concerns you, don't worry; we can do much better. JIT Props To optimize our bundled styles, we can use a PostCSS plugin called posts-jit-props. This package will ensure that we only ship the props that we are using. Vite has support for PostCSS, so setting it up is straightforward. Let's install the plugin: ` After the installation finishes, let's create a configuration file to include it. ` The content of your file should look like this: ` Finally, remove the open-props/style import from styles.css. Remember that this file contains the CSS variables we will add "just in time". Our page should still look the same, but if we analyze the size of our styles.css file again, we can see that it has already been reduced to 13.2kb. If you want to know where this size is coming from, the answer is that Open Props is adding all the variables used in the normalize file + the ones that we require in our file. If we were to remove the normalize import, we would end up with much smaller CSS files, and the number of props added just in time would be minimal. Try removing commenting it out (the open-props/normalize import) from the styles.css file. The page will look different, but it will be useful to show how just the props used are added. 2.4kB uncompressed. That's a lot less for our example. If we take a quick look at our generated file, we can see the small list of CSS variables added from open props at the top of our file (those that we use later on the file). Open props ships with tons of variables for: - Colors - Gradients - Shadows - Aspect Ratios - Typography - Easing - Animations - Sizes - Borders - Z-Index - Media Queries - Masks You probably won't use all of these but it's hard to tell what you'll be using from the beginning of a project. To keep things light, add what you need as you go, or let JIT handle it for you. Conclusion Open props has much to offer and can help speed your project by leveraging some decisions upfront and providing a sensible set of predefined CSS Variables. We've learned how to install it (or not) using different methods and showcased how simple it is to use. Give it a try!...

Lessons from the DOGE Website Hack: How to Secure Your Next.js Website cover image

Lessons from the DOGE Website Hack: How to Secure Your Next.js Website

Lessons from the DOGE Website Hack: How to Secure Your Next.js Website The Department of Government Efficiency (DOGE) launched a new website, doge.gov. Within days, it was defaced with messages from hackers. The culprit? A misconfigured database was left open, letting anyone edit content. Reports suggest the site was built on Cloudflare Pages, possibly with a Next.js frontend pulling data dynamically. While we don’t have the tech stack confirmed, we are confident that Next.js was used from early reporting around the website. Let’s dive into what went wrong—and how you can secure your own Next.js projects. What Happened to DOGE.gov? The hack was a classic case of security 101 gone wrong. The database—likely hosted in the cloud—was accessible without authentication. No passwords, no API keys, no nothing. Hackers simply connected to it and started scribbling their graffiti. Hosted on Cloudflare Pages (not government servers), the site might have been rushed, skipping critical security checks. For a .gov domain, this is surprising—but it’s a reminder that even big names can miss best practices. It’s easy to imagine how this happened: an unsecured server action is being used on the client side, a serverless function or API route fetching data from an unsecured database, no middleware enforcing access control, and a deployment that didn’t double-check cloud configs. Let’s break down how to avoid this in your own Next.js app. Securing Your Next.js Website: 5 Key Steps Next.js is a powerhouse for building fast, scalable websites, but its flexibility means you’re responsible for locking the doors. Here’s how to keep your site safe. 1. Double-check your Server Actions If Next.js 13 or later was used, Server Actions might’ve been part of the mix—think form submissions or dynamic updates straight from the frontend. These are slick for handling server-side logic without a separate API, but they’re a security risk if not handled right. An unsecured Server Action could’ve been how hackers slipped into the database. Why? Next.js generates a public endpoint for each Server Action. If these Server Actions lack proper authentication and authorization measures, they become vulnerable to unauthorized data access. Example: * Restrict Access: Always validate the user’s session or token before executing sensitive operations. * Limit Scope: Only allow Server Actions to perform specific, safe tasks—don’t let them run wild with full database access. * Don’t use server action on the client side without authorization and authentication checks 2. Lock Down Your Database Access Another incident happened in 2020. A hacker used an automated script to scan for misconfigured MongoDB databases, wiping the content of 23 thousand databases that have been left wide open, and leaving a ransom note behind asking for money. So whether you’re using MongoDB, PostgreSQL, or Cloudflare’s D1, never leave it publicly accessible. Here’s what to do: * Set Authentication: Always require credentials (username/password or API keys) to connect. Store these in environment variables (e.g., .env.local for Next.js) and access them via process.env. * Whitelist IPs: If your database is cloud-hosted, restrict access to your Next.js app’s server or Vercel deployment IP range. * Use VPCs: For extra security, put your database in a Virtual Private Cloud (VPC) so it’s not even exposed to the public internet. If you are using Vercel, you can create private connections between Vercel Functions and your backend cloud, like databases or other private infrastructure, using Vercel Secure Compute Example: In a Next.js API route (/app/api/data.js): ` > Tip: Don’t hardcode MONGO_URI—keep it in .env and add .env to .gitignore. 3. Secure Your API Routes Next.js API routes are awesome for server-side logic, but they’re a potential entry point if left unchecked. The site might’ve had an API endpoint feeding its database updates without protection. * Add Authentication: Use a library like next-auth or JSON Web Tokens (JWT) to secure routes. * Rate Limit: Prevent abuse with something like rate-limiter-flexible. Example: ` 4. Double-Check Your Cloud Config A misconfigured cloud setup may have exposed the database. If you’re deploying on Vercel, Netlify, or Cloudflare: * Environment Variables: Store secrets in your hosting platform’s dashboard, not in code. * Serverless Functions: Ensure they’re not leaking sensitive data in responses. Log errors, not secrets. * Access Controls: Verify your database firewall rules only allow connections from your app. 5. Sanitize and Validate Inputs Hackers love injecting junk into forms or APIs. If your app lets users submit data (e.g., feedback forms), unvalidated inputs could’ve been a vector. In Next.js: * Sanitize: Use libraries like sanitize-html for user inputs. * Validate: Check data types and lengths before hitting your database. Example: ` Summary The DOGE website hack serves as a reminder of the ever-present need for robust security measures in web development. By following the outlined steps–double-checking Server Actions, locking down database access, securing API routes, verifying cloud configurations, and sanitizing/validating inputs–you can enhance the security posture of your Next.js applications and protect them from potential threats. Remember, a proactive approach to security is always the best defense....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co