<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: James Perkins</title>
    <description>The latest articles on DEV Community by James Perkins (@perkinsjr).</description>
    <link>https://dev.to/perkinsjr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F127679%2Fabc7cce4-8d64-403a-aef2-b16529691702.jpg</url>
      <title>DEV Community: James Perkins</title>
      <link>https://dev.to/perkinsjr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/perkinsjr"/>
    <language>en</language>
    <item>
      <title>How to ratelimit tRPC routes with Unkey</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Fri, 17 May 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/unkey/how-to-ratelimit-trpc-routes-with-unkey-ppi</link>
      <guid>https://dev.to/unkey/how-to-ratelimit-trpc-routes-with-unkey-ppi</guid>
      <description>&lt;p&gt;Ratelimiting is not just a feature; it's a lifeline for production applications. Without it, you could face a skyrocketing bill. Your server could be pushed to its limits, leaving real users stranded and your application's reputation at stake.&lt;/p&gt;

&lt;p&gt;Unkey provides ratelimiting that is distributed globally and can be easily added to any server, ensuring your protection. We will discuss the features of our service, including synchronous and asynchronous protection, identifier overrides, and how our analytical data can help you identify spikes in usage and how it can be used in a tRPC application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;To get up and running with the app and follow along, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fundamental understanding of Next.js, primarily regarding routes and server-side data loading.&lt;/li&gt;
&lt;li&gt;Basic familiarity with tRPC.&lt;/li&gt;
&lt;li&gt;An application with user authentication implemented. This example uses Auth.js, but you can use any provider you like.&lt;/li&gt;
&lt;li&gt;Access to your &lt;code&gt;UNKEY_ROOT_KEY&lt;/code&gt;, which you can get by signing up for a free account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, we will use create-t3-app for the demo, so feel free to use that if you want an easy way to use tRPC + Auth.js in a Next.js application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the &lt;code&gt;@unkey/ratelimit&lt;/code&gt; package
&lt;/h2&gt;

&lt;p&gt;Before we start coding, we need to install the &lt;code&gt;@unkey/ratelimit&lt;/code&gt; package. This package gives you access to Unkey's API with type safety.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @unkey/ratelimit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating our env
&lt;/h3&gt;

&lt;p&gt;We need to use the &lt;code&gt;UNKEY_ROOT_KEY&lt;/code&gt; to run our ratelimiting package, so we must first update the &lt;code&gt;env.js&lt;/code&gt; file in the &lt;code&gt;src&lt;/code&gt; directory. Add &lt;code&gt;UNKEY_ROOT_KEY: z.string()&lt;/code&gt; to the &lt;code&gt;server&lt;/code&gt; object and &lt;code&gt;UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY&lt;/code&gt; to the &lt;code&gt;runtimeEnv&lt;/code&gt; object.&lt;/p&gt;

&lt;p&gt;Now that it is updated add your Unkey root key to your .env as &lt;code&gt;UNKEY_ROOT_KEY&lt;/code&gt; which can be found in the Unkey dashboard under settings Root Keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding ratelimiting to a procedure
&lt;/h2&gt;

&lt;p&gt;Now that the package is installed and our &lt;code&gt;.env&lt;/code&gt; has been updated, we can configure our ratelimiter. Inside the &lt;code&gt;server/api/routers/post&lt;/code&gt; file, we have a &lt;code&gt;create&lt;/code&gt; procedure. This procedure allows users to create posts; currently, users can create as many as they like and as quickly as they like.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure our ratelimiter
&lt;/h3&gt;

&lt;p&gt;In this example, we will configure our ratelimiter in the procedure itself. Of course, you can abstract this into a utility file if you prefer. First, we must import &lt;code&gt;Ratelimit&lt;/code&gt; from the &lt;code&gt;@unkey/ratelimit&lt;/code&gt; package and &lt;code&gt;TRPCError&lt;/code&gt; and &lt;code&gt;env&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";import { createTRPCRouter, protectedProcedure, publicProcedure,} from "~/server/api/trpc";import { posts } from "~/server/db/schema";import { env } from "~/env";import { TRPCError } from "@trpc/server";import { Ratelimit } from "@unkey/ratelimit";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To configure the Ratelimiter, we need to pass four things along, the root key, the namespace, the limit, and the duration of our ratelimiting. Inside the mutation, add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const unkey = new Ratelimit({ rootKey: env.UNKEY_ROOT_KEY, namespace: "posts.create", limit: 3, duration: "5s",});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The namespace can be anything, but we are using the tRPC route and procedure to make it easier to track in Unkey's analytics. We now have the ability to rate-limit this procedure, allowing only three requests per five seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using our ratelimiting
&lt;/h3&gt;

&lt;p&gt;To use the ratelimit, we need an identifier. This can be anything you like, such as a user ID or an IP address. We will be using our user's ID as they are required to be logged in to create a new post. Then, we can call &lt;code&gt;unkey.limit&lt;/code&gt; with the identifier, and unkey will return a boolean of true or false, which we can use to make a decision.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { success } = await unkey.limit(ctx.session.user.id);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now we have the boolean we can check if it's false and then throw a TRPCError telling the user they have been ratelimited and stop any more logic running.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { success } = await unkey.limit(ctx.session.user.id);if (!success) { throw new TRPCError({ code: "TOO_MANY_REQUESTS" });}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, our code is ready to test. Give it a whirl, and try posting multiple times. You will see that the posts won't update anymore after you are rate-limited.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about more expensive requests?
&lt;/h2&gt;

&lt;p&gt;Unkey allows you to tell us how expensive a request should be. For example, maybe you have an AI route that costs you a lot more than any other route, so you want to reduce the number of requests that can be used.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const { success } = await unkey.limit(ctx.session.user.id, { cost: 3,});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This request costs three instead of one, giving you extra flexibility around expensive routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Faster response
&lt;/h2&gt;

&lt;p&gt;Although Unkey response times are fast, there are some cases where you are willing to give up some accuracy in favor of quicker response times. You can use our &lt;code&gt;async&lt;/code&gt; option, which has 98% accuracy, but we don't need to confirm the limit with the origin before returning a decision. You can set this either on the &lt;code&gt;limit&lt;/code&gt; request or on the configuration itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const unkey = new Ratelimit({ rootKey: env.UNKEY_ROOT_KEY, namespace: "posts.create", limit: 3, duration: "5s", async: true,});// orconst { success } = await unkey.limit(ctx.session.user.id, { async: true,});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this is a small overview of using Unkey's ratelimiting with tRPC, we also offer other features that aren't covered here, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Overrides for specific identifiers&lt;/li&gt;
&lt;li&gt;Metadata&lt;/li&gt;
&lt;li&gt;Resources flagging&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can read more about those features in our documentation on &lt;a href="https://www.unkey.com/docs/ratelimiting/introduction" rel="noopener noreferrer"&gt;Ratelimiting&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>ratelimit</category>
      <category>otp</category>
    </item>
    <item>
      <title>How to build authentic communication in your team</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Fri, 23 Feb 2024 00:00:00 +0000</pubDate>
      <link>https://dev.to/perkinsjr/how-to-build-authentic-communication-in-your-team-5f9k</link>
      <guid>https://dev.to/perkinsjr/how-to-build-authentic-communication-in-your-team-5f9k</guid>
      <description>&lt;p&gt;Before starting Unkey, I worked at several companies ranging in size from 5 to 5,000 employees, and one of the biggest hurdles a company can face is how to encourage authentic communication.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is authentic communication?
&lt;/h2&gt;

&lt;p&gt;Authentic communication has been covered many times over the years, but the idea has a few core concepts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Being able to communicate in the style that fits you&lt;/li&gt;
&lt;li&gt;Being able to be your genuine self regardless of the interaction you are having&lt;/li&gt;
&lt;li&gt;Being able to have a difficult conversation and not feel you are being dismissed&lt;/li&gt;
&lt;li&gt;Listening to other parties to understand their point of view&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How do you build this into a team?
&lt;/h2&gt;

&lt;p&gt;Building authentic communication into a team requires a lot of work and understanding, but it pays off in the long run for the health of the team and the company. Here are some steps that can help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model the behavior:&lt;/strong&gt; Leaders in the organization or team should lead by example when communicating with others. They should practice active listening, be open to feedback, and show empathy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a safe environment:&lt;/strong&gt; The team should feel safe to express their thoughts and feelings without fear of judgment or retribution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encourage feedback:&lt;/strong&gt; Regular feedback sessions help team members feel heard and valued. It can also provide opportunities to address any issues or misunderstandings. It also is a great place to come up with new ideas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Celebrate diversity:&lt;/strong&gt; Each team member brings a unique perspective and communication style. Celebrating these differences can encourage more authentic communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While all of these may seem obvious when the team is smaller, ensuring everyone has an equal opportunity to share their thoughts becomes harder when the team grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  How are we attempting this at Unkey?
&lt;/h2&gt;

&lt;p&gt;Unkey lives on being open from the top; Andreas and I ensure everyone understands how Unkey runs, what we expect, and the company's direction. So, for our team to feel empowered to be their authentic self when communicating, we do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Updates:&lt;/strong&gt; Every update we send out to investors and customers is public and can be accessed by the team; I intentionally send a Slack message every month in the general channel. These updates include our current status for KPIs, the money we have spent, the money we have left, usage, and paying customers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open communication:&lt;/strong&gt; We communicate in the general channel using threads to keep things tidy, whether it is an idea, direction, or potentially something we are concerned about. This allows the team to provide their feedback, concerns, or excitement.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meetings are for everyone:&lt;/strong&gt; Unkey only runs two meetings as a team; one is planning every Monday, and the other is demo every other Friday. These meetings are open forums and flexible; the team can provide their thoughts about what is up next for us and things they want to work on and provide input on ideas they might have. After anything is demoed, we open the floor for discussions surrounding the feature; we can talk about the code behind it and changes that might improve it, knowing that feedback is welcomed. We also ensure everyone gets time on the floor so no one is left out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open door policies:&lt;/strong&gt; Andreas and I have an open door policy, albeit more of an open Slack policy, which could lead to a meeting. Our team can come to us with concerns, problems they are facing, feedback, and ideas at any point and communicate them to us however they want.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedicated space:&lt;/strong&gt; Andreas meets with the developers twice a month, and I meet with them once a month. This time is for them and not for us. They can come with technical questions, business questions, vision questions, or to chat about how they are feeling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I understand that our current implementation wont scale past 20 or 30 team members; we will tweak this to allow for authenticated communication as we keep this a core value at Unkey.&lt;/p&gt;

</description>
      <category>ceo</category>
      <category>company</category>
    </item>
    <item>
      <title>The UX of UUIDs</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Thu, 07 Dec 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/perkinsjr/the-ux-of-uuids-2e1j</link>
      <guid>https://dev.to/perkinsjr/the-ux-of-uuids-2e1j</guid>
      <description>&lt;p&gt;TLDR: Please don't do this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://company.com/resource/c6b10dd3-1dcf-416c-8ed8-ae561807fcaf&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The baseline: Ensuring global uniqueness
&lt;/h2&gt;

&lt;p&gt;Unique identifiers are essential for distinguishing individual entities within a system. They provide a reliable way to ensure that each item, user, or piece of data has a unique identity. By maintaining uniqueness, applications can effectively manage and organize information, enabling efficient operations and facilitating data integrity.&lt;/p&gt;

&lt;p&gt;Lets not pretend like we are Google or AWS who have special needs around this. Any securely generated UUID with 128 bits is more than enough for us. There are lots of libraries that generate one, or you could fall back to the standard library of your language of choice. In this blog, I'll be using Typescript examples, but the underlying ideas apply to any language.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const id = crypto.randomUUID();// '5727a4a4-9bba-41ae-b7fe-e69cf60bb0ab'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stopping here is an option, but let's take the opportunity to enhance the user experience with small yet effective iterative changes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Make them easy to copy&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prefixing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;More efficient encoding&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Changing the length&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Copying UUIDs is annoying
&lt;/h3&gt;

&lt;p&gt;Try copying this UUID by double-clicking on it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;c6b10dd3-1dcf-416c-8ed8-ae561807fcaf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're lucky, you got the entire UUID but for most people, they got a single section. One way to enhance the usability of unique identifiers is by making them easily copyable. This can be achieved by removing the hyphens from the UUIDs, allowing users to simply double-click on the identifier to copy it. By eliminating the need for manual selection and copy-pasting, this small change can greatly improve the user experience when working with identifiers.&lt;/p&gt;

&lt;p&gt;Removing the hyphens is probably trivial in all languages, heres how you can do it in js/ts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const id = crypto.randomUUID().replace(/-/g, "");// fe4723eab07f408384a2c0f051696083
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try copying it now, its much nicer!&lt;/p&gt;

&lt;h3&gt;
  
  
  Prefixing
&lt;/h3&gt;

&lt;p&gt;Have you ever accidentally used a production API key in a development environment? I have, and its not fun. We can help the user differentiate between different environments or resources within the system by adding a meaningful prefix. For example, Stripe uses prefixes like &lt;code&gt;sk_live_&lt;/code&gt; for production environment secret keys or &lt;code&gt;cus_&lt;/code&gt; for customer identifiers. By incorporating such prefixes, we can ensure clarity and reduce the chances of confusion, especially in complex systems where multiple environments coexist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const id = `hello_${crypto.randomUUID().replace(/-/g, "")}`;// hello_1559debea64142f3b2d29f8b0f126041
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naming prefixes is an art just like naming variables. You want to be descriptive but be as short as possible. I'll share ours further down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Encoding in base58
&lt;/h3&gt;

&lt;p&gt;Instead of using a hexadecimal representation for identifiers, we can also consider encoding them more efficiently, such as base58. Base58 encoding uses a larger character set and avoids ambiguous characters, such as upper case &lt;code&gt;I&lt;/code&gt; and lower case &lt;code&gt;l&lt;/code&gt; resulting in shorter identifier strings without compromising readability.&lt;/p&gt;

&lt;p&gt;As an example, an 8-character long base58 string, can store roughly 30.000 times as many states as an 8-char hex string. And at 16 chars, the base58 string can store 889.054.070 as many combinations.&lt;/p&gt;

&lt;p&gt;You can probably still do this with the standard library of your language but you could also use a library like &lt;a href="https://github.com/ai/nanoid" rel="noopener noreferrer"&gt;nanoid&lt;/a&gt; which is available for most languages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { customAlphabet } from "nanoid";export const nanoid = customAlphabet( "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",);const id = `prefix_${nanoid(22)}`;// prefix_KSPKGySWPqJWWWa37RqGaX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We generated a 22 character long ID here, which can encode ~100x as many states as a UUID while being 10 characters shorter.&lt;/p&gt;

&lt;p&gt;| | Characters | Length | Total States |&lt;br&gt;
| UUID | 16 | 32 | 2^122 = 5.3e+36 |&lt;br&gt;
| Base58 | 58 | 22 | 58^22 = 6.2e+38 |&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The more states, the higher your collision resistance is because it takes more generations to generate the same ID twice (on average and if your algorithm is truly random)&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Changing the entropy
&lt;/h3&gt;

&lt;p&gt;Not all identifiers need to have a high level of collision resistance. In some cases, shorter identifiers can be sufficient, depending on the specific requirements of the application. By reducing the entropy of the identifiers, we can generate shorter IDs while still maintaining an acceptable level of uniqueness.&lt;/p&gt;

&lt;p&gt;Reducing the length of your IDs can be nice, but you need to be careful and ensure your system is protected against ID collissions. Fortunately, this is pretty easy to do in your database layer. In our MySQL database we use IDs mostly as primary key and the database protects us from collisions. In case an ID exists already, we just generate a new one and try again. If our collision rate would go up significantly, we could simply increase the length of all future IDs and wed be fine.&lt;/p&gt;

&lt;p&gt;| Length | Example | Total States |&lt;br&gt;
| nanoid(8) | re6ZkUUV | 1.3e+14 |&lt;br&gt;
| nanoid(12) | pfpPYdZGbZvw | 1.4e+21 |&lt;br&gt;
| nanoid(16) | sFDUZScHfZTfkLwk | 1.6e+28 |&lt;br&gt;
| nanoid(24) | u7vzXJL9cGqUeabGPAZ5XUJ6 | 2.1e+42 |&lt;br&gt;
| nanoid(32) | qkvPDeH6JyAsRhaZ3X4ZLDPSLFP7MnJz | 2.7e+56 |&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By implementing these improvements, we can enhance the usability and efficiency of unique identifiers in our applications. This will provide a better experience for both users and developers, as they interact with and manage various entities within the system. Whether it's copying identifiers with ease, differentiating between different environments, or achieving shorter and more readable identifier strings, these strategies can contribute to a more user-friendly and robust identification system.&lt;/p&gt;
&lt;h2&gt;
  
  
  IDs and keys at Unkey
&lt;/h2&gt;

&lt;p&gt;Lastly, I'd like to share our implementation here and how we use it in our &lt;a href="https://github.com/unkeyed/unkey/blob/main/internal/id/src/index.ts" rel="noopener noreferrer"&gt;codebase&lt;/a&gt;. We use a simple function that takes a typed prefix and then generates the ID for us. This way we can ensure that we always use the same prefix for the same type of ID. This is especially useful when you have multiple types of IDs in your system.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { customAlphabet } from "nanoid";export const nanoid = customAlphabet( "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz",);const prefixes = { key: "key", api: "api", policy: "pol", request: "req", workspace: "ws", keyAuth: "key_auth", // &amp;lt;-- this is internal and does not need to be short or pretty vercelBinding: "vb", test: "test", // &amp;lt;-- for tests only} as const;export function newId(prefix: keyof typeof prefixes): string { return [prefixes[prefix], nanoid(16)].join("_");}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when we use it in our codebase, we can ensure that we always use the correct prefix for the correct type of id.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { newId } from "@unkey/id";const id = newId("workspace");// ws_dYuyGV3qMKvebjMLconst id = newId("keyy");// invalid because `keyy` is not a valid prefix name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;I've been mostly talking about identifiers here, but an api key really is just an identifier too. It's just a special kind of identifier that is used to authenticate requests. We use the same strategies for our api keys as we do for our identifiers. You can add a prefix to let your users know what kind of key they are looking at and you can specify the length of the key within reason. Colissions for API keys are much more serious than ids, so we enforce secure limits.&lt;/p&gt;

&lt;p&gt;It's quite common to prefix your API keys with something that identifies your company. For example &lt;a href="https://resend.com" rel="noopener noreferrer"&gt;Resend&lt;/a&gt; are using &lt;code&gt;re_&lt;/code&gt; and &lt;a href="https://openstatus.dev" rel="noopener noreferrer"&gt;OpenStatus&lt;/a&gt; are using &lt;code&gt;os_&lt;/code&gt; prefixes. This allows your users to quickly identify the key and know what it's used for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const key = await unkey.key.create({ apiId: "api_dzeBEZDwJ18WyD7b", prefix: "blog", byteLength: 16, // ... omitted for brevity});// Created key:// blog_cLsvCvmY35kCfchi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;]]&amp;gt;&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>uuid</category>
    </item>
    <item>
      <title>Unkey raises 1.5 million pre-seed</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Wed, 15 Nov 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/unkey/unkey-raises-15-million-pre-seed-5f4n</link>
      <guid>https://dev.to/unkey/unkey-raises-15-million-pre-seed-5f4n</guid>
      <description>&lt;p&gt;The Unkey pre-seed round was led by &lt;a href="https://www.essencevc.fund/" rel="noopener noreferrer"&gt;Essence VC&lt;/a&gt; and joined by &lt;a href="https://www.linkedin.com/in/liujiang1/" rel="noopener noreferrer"&gt;Sunflower Capital&lt;/a&gt;, &lt;a href="https://new-normal.ventures/" rel="noopener noreferrer"&gt;The Normal Fund&lt;/a&gt;, and some fantastic friends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essence VC leads our round.
&lt;/h2&gt;

&lt;p&gt;When we met Tim Chen for the first time, Andreas and I were blown away by his experience and his immediate understanding of our vision. We only needed ten minutes before we were bouncing ideas and thinking about where to go with Unkey.&lt;/p&gt;

&lt;p&gt;Tim also introduced us to our other partners (Sunflower Capital and The New Normal Fund), who were just as excited to see how API authentication and authorization could be simplified.&lt;/p&gt;

&lt;h3&gt;
  
  
  Angels
&lt;/h3&gt;

&lt;p&gt;Our angels are a fantastic group of people in no particular order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Andrew Miklas (Functional Capital / Ex PageDuty Co Founder)&lt;/li&gt;
&lt;li&gt;Ant Wilson &amp;amp; Rory Wilding (Supabase Co Founder / Head of Growth at Supabase)&lt;/li&gt;
&lt;li&gt;Paul Copplestone (Supabase Co Founder)&lt;/li&gt;
&lt;li&gt;Theo Browne (Ping / YouTube / Twitch / Twitter)&lt;/li&gt;
&lt;li&gt;Ian Livingstone (Startup Advisor / Investor)&lt;/li&gt;
&lt;li&gt;Preston-Werner Ventures (Led by Tom Preston-Werner Co-Founder of GitHub)&lt;/li&gt;
&lt;li&gt;George &amp;amp; Chris Perkins (James mum and dad)&lt;/li&gt;
&lt;li&gt;Zain Allarakhia (ex Pipe)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it all started
&lt;/h2&gt;

&lt;p&gt;Andreas and I started Unkey in the middle of June and launched on June 21st, the community loved Unkey and we got some awesome feedback. Once we understood that this side project, which started as a way to solve an annoying problem we both had experienced, was needed by more and more people, we set our sights on building an entire business for it.&lt;/p&gt;

&lt;p&gt;We worked nights and weekends for months to ensure Unkey made scaling user-facing APIs more accessible than ever. Shout out to all the friends and family who supported us getting through the 16+ hour days.&lt;/p&gt;

&lt;h2&gt;
  
  
  How's it going
&lt;/h2&gt;

&lt;p&gt;Since the launch in June, we have had success with the product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.2k stars on &lt;a href="https://github.com/unkeyed/unkey" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;We have done over 13 million API key verifications.&lt;/li&gt;
&lt;li&gt;1.6k users have signed up&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Now that Unkey is funded, we can focus on building the best developer experience in the API space. You may be wondering what is next.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hiring 1-2 more engineers.&lt;/li&gt;
&lt;li&gt;Create an easy-to-use permission system that allows you to control access through RBAC, ABAC, or REBAC.&lt;/li&gt;
&lt;li&gt;Create a new gateway system that allows any developer to understand and configure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Talking about hiring, if you are interested in being part of the team. Check out the &lt;a href="https://dev.to/careers"&gt;job posting and apply&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thank you for all the support, and we look forward to bringing API scalability to developers like never before.&lt;/p&gt;

</description>
      <category>preseed</category>
      <category>vcs</category>
    </item>
    <item>
      <title>Why we built Unkey</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Tue, 04 Jul 2023 20:59:35 +0000</pubDate>
      <link>https://dev.to/perkinsjr/why-we-built-unkey-4b2l</link>
      <guid>https://dev.to/perkinsjr/why-we-built-unkey-4b2l</guid>
      <description>&lt;h2&gt;
  
  
  What is Unkey?
&lt;/h2&gt;

&lt;p&gt;Unkey provides a way for you to create, manage and revoke API Keys that belong to your API. It's a simple concept, but one that we think is important. We built Unkey to layer it into your API so that you can issue keys, manage them and revoke them as needed. This means we need a simple and great DX and fast response times.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why did we build it?
&lt;/h2&gt;

&lt;p&gt;We built Unkey because we were tired of copying and pasting the same API management into a new codebase. Similar to authentication, you can do it yourself, but having a dedicated company that is working on improvements, and worrying about security and latency means you can focus on the business and core features.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the current features?
&lt;/h2&gt;

&lt;p&gt;Unkey provides the following features out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;An admin dashboard to manage your APIs, issue keys, revoke keys and see usage stats.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A simple API with SDKs for typescript&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Short-lived keys&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rate limited keys&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata attached to keys&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deep dive into the features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Admin dashboard
&lt;/h3&gt;

&lt;p&gt;Our admin dashboard gives you access to several features in a simple-to-use interface. You can create new APIs, issue keys, revoke keys and see usage stats. You can also invite other users to your account so that they can manage your APIs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0r7vkatxeqy0y8u5q82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu0r7vkatxeqy0y8u5q82.png" alt="Admin dashboard" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple API with SDKs
&lt;/h3&gt;

&lt;p&gt;We wanted the DX for Unkey to be as simple as possible. We've built a simple API that you can use to create, manage and revoke keys. We wanted to make sure the API was easy to integrate and didn't take engineering time away.&lt;/p&gt;

&lt;p&gt;No API is great without SDKs so we built a typescript one and our community also built an Elixir and Python SDK.&lt;/p&gt;

&lt;h3&gt;
  
  
  Short-lived keys
&lt;/h3&gt;

&lt;p&gt;When we started Unkey we wanted the ability to issue a short-lived key but we didn't want to restrict the period. We understand that each use case is different so you can pass us an expiration or select it in the UI.&lt;/p&gt;

&lt;p&gt;Once the key expires, we revoke the key and the user can no longer access your content. This is great for audit teams, trials, or payment systems that give you access for some time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate limited keys
&lt;/h3&gt;

&lt;p&gt;Rate limiting is essential to all businesses that have an API. Unkey gives you the ability to set each key to a different rate limit. We handle the rate limiting for you, it's as simple as telling us the total amount of burstable requests, the refill rate, and how quickly they should be refilled.&lt;/p&gt;

&lt;p&gt;Once you have the key with the limits, with each request we return the amount the request limit, how many remaining, and the reset time. Once a user hits the rate limit we will return &lt;code&gt;json "valid": false&lt;/code&gt; alongside a code &lt;code&gt;RATELIMITED&lt;/code&gt; if the rate limit has been hit, so you know that this user shouldn't be able to access your endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata attached to keys
&lt;/h3&gt;

&lt;p&gt;We wanted to give you the ability to attach metadata to your keys so you can easily make business decisions based on the data instead of having to look up the key in your database. You can include anything that you want to read on the server, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"billingTier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"PRO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"trialEnds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-06-16T17:16:37.161Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows you to pull less data from other systems in your infrastructure to make business decisions faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built with speed in mind
&lt;/h2&gt;

&lt;p&gt;Unkey was built to have minimal impact on your API, we have our database in the United States but replicate this into other regions so that we can serve your requests as fast as possible. We have our API distributed across the world so that requests will go to the closest region to your user, process at the closet database, and then return the response. Unkey also automatically caches the key on creation, so that the first request is as fast as the second or third request.&lt;/p&gt;

&lt;p&gt;On average our requests add less than 40ms to your requests, below are some of the latest requests we are monitoring through a product called &lt;a href="https://planetfall.io" rel="noopener noreferrer"&gt;Planetfall&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvtgfn31euimks95mpho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvtgfn31euimks95mpho.png" alt="Unkey latency" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ready to get started? Head over to &lt;a href="https://unkey.dev/app" rel="noopener noreferrer"&gt;Unkey&lt;/a&gt; and get started for free today.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>api</category>
      <category>tools</category>
    </item>
    <item>
      <title>Rate limiting in Next.js in under 10 minutes</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Thu, 23 Mar 2023 08:00:00 +0000</pubDate>
      <link>https://dev.to/perkinsjr/rate-limiting-in-nextjs-in-under-10-minutes-12ab</link>
      <guid>https://dev.to/perkinsjr/rate-limiting-in-nextjs-in-under-10-minutes-12ab</guid>
      <description>&lt;p&gt;Rate limiting is an important feature that you need in production applications. It may sound scary, but it's actually fairly easy to implement API rate limiting in today's world. In this blog post, we'll walk through the process of implementing rate limiting using Upstash and Redis.&lt;/p&gt;

&lt;p&gt;In this example we are using my &lt;a href="https://github.com/perkinsjr/t3-app-clerk-minimal" rel="noopener noreferrer"&gt;t3-clerk-minimal app&lt;/a&gt; that you can find here. Make sure you have your &lt;a href="https://dashboard.clerk.com" rel="noopener noreferrer"&gt;Clerk&lt;/a&gt; keys in your application before starting.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/aIJVheYy5eo"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Upstash?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.jamesperkins.dev%2Fimages%2Frate-limit%2Fupstash.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.jamesperkins.dev%2Fimages%2Frate-limit%2Fupstash.png" alt="upstash-home"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upstash is a serverless data stack that can be served to you on the edge using Redis or Qstash. Most people prefer Redis, and it has a really good free tier. We're going to use Upstash to implement rate limiting because they have a rate limit package that makes it dead simple to implement anywhere that you're using APIs.&lt;/p&gt;

&lt;p&gt;First, you need to sign up for an account. Once you've done that, you'll need to click on your console and create a new database. Give it a name, and set the region to the correct region for your area (e.g. US East One or US West).&lt;/p&gt;

&lt;p&gt;You can enable TLS if you want. Go ahead and create your Redis for Upstash.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the ENV
&lt;/h3&gt;

&lt;p&gt;Now that your Redis instance has been created, you'll need to set up the ENV with your Redis REST URL and token. You can copy these information from the Upstash console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.jamesperkins.dev%2Fimages%2Frate-limit%2Fenv-image.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fwww.jamesperkins.dev%2Fimages%2Frate-limit%2Fenv-image.png" alt="env"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From here, go to your IDE (e.g. Visual Studio Code) and add a &lt;code&gt;.env&lt;/code&gt; file. You should already have your Upstash URLs and token in there. Close the &lt;code&gt;.env&lt;/code&gt; file once you've pasted your Redis instance details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Clerk for User Authentication
&lt;/h2&gt;

&lt;p&gt;If you haven't used &lt;a href="https://clerk.dev/" rel="noopener noreferrer"&gt;Clerk&lt;/a&gt; before, it's a user authentication and management system for the modern web, supporting frameworks like Next.js, Remix, and Gatsby. In this example, we'll be using Clerk for user authentication and protected routes.&lt;/p&gt;

&lt;p&gt;To implement rate limiting, we need an identifier for the user (e.g. user ID, IP address) to determine if the same user is making multiple requests. We'll use the user ID from Clerk for this purpose.&lt;/p&gt;



&lt;h3&gt;
  
  
  Creating the Rate Limiter
&lt;/h3&gt;

&lt;p&gt;First, we need to install two packages, &lt;code&gt;upstash-rate-limit&lt;/code&gt; and &lt;code&gt;upstash-redis&lt;/code&gt;. You can do so by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install @upstash/ratelimit @upstash/redis

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, import both packages in your &lt;a href="https://github.com/perkinsjr/t3-app-clerk-minimal/blob/main/src/server/api/routers/example.ts" rel="noopener noreferrer"&gt;example router&lt;/a&gt; and initialize the rate limiter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import RateLimit from '@upstash/ratelimit';
import Redis from '@upstash/redis';

const rateLimiter = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(2, "3 s")
});

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example sets a rate limit of 2 requests per 3 seconds. You can adjust the values as needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the Rate Limiter in a Protected Route
&lt;/h3&gt;

&lt;p&gt;Now, we will implement the rate limiter in a protected route. First, create your route (in this example, we'll call it &lt;code&gt;expensive&lt;/code&gt;). Inside the route, use the rate limiter with the user ID from Clerk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { TrpcError } from '@trpc/server';

// other routes
 expensive: protectedProcedure.query(async ({ ctx }) =&amp;gt; {
    const { success } = await rateLimiter.limit(ctx.auth.userId);
    if (!success) {
      throw new TRPCError({ code: "TOO_MANY_REQUESTS" })
    }
    return "expensive"
  })

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code checks whether the rate limit has been exceeded; if it has, a "Too Many Requests" error is thrown.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing the Rate Limiter
&lt;/h2&gt;

&lt;p&gt;Now that the rate limiter has been implemented in your application, you can test your protected route to ensure the rate limiting is working as intended. In this example, we will test the rate limiter using a "click here to protected procedure" button on a web page.&lt;/p&gt;

&lt;p&gt;Run your application with &lt;code&gt;npm run dev&lt;/code&gt;, and navigate to your localhost URL. Click the button, sign in using your preferred authentication method, and you should be redirected to the protected page. Open your browser console and refresh the page - you should encounter a "Too Many Requests" error if you refresh too often.&lt;/p&gt;

&lt;p&gt;If you wait a few seconds and refresh again, the error should be gone, indicating that the rate limiting is working as intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this blog post, we've demonstrated how to implement rate limiting using Upstash and Redis, as well as integrating the rate limiter with Clerk for user authentication. Implementing rate limiting is an important aspect of maintaining a secure and stable production application, and this guide should help you get started in adding it to your own projects.&lt;/p&gt;

&lt;p&gt;If you found this guide helpful, please consider subscribing to my Youtube channel or the newsletter for more tips and tutorials for web development. Happy coding!&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>redis</category>
      <category>upstash</category>
      <category>ratelimit</category>
    </item>
    <item>
      <title>Zod Typesafe User Input</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Sat, 26 Nov 2022 17:24:50 +0000</pubDate>
      <link>https://dev.to/perkinsjr/zod-typesafe-user-input-4pll</link>
      <guid>https://dev.to/perkinsjr/zod-typesafe-user-input-4pll</guid>
      <description>&lt;h2&gt;
  
  
  What is Zod?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;Zod&lt;/a&gt; is a TypeScript-first schema declaration and validation library. It ensures that your data is valid before it is submitted to the server, which means less invalid data and fewer errors on your end. Additionally, Zod allows you to create custom input types to tailor the user experience to your specific needs. Overall, using Zod can help streamline your development process by making sure that your forms, inputs, and API requests are error-free from the start. If you're looking for a reliable way to create typesafe forms and inputs, Zod is worth checking out!&lt;/p&gt;

&lt;h2&gt;
  
  
  How can Zod help with type safety in forms and input from users?
&lt;/h2&gt;

&lt;p&gt;Zod can help ensure that your forms and input are type-safe, so you won't accidentally allow incorrect data types to be entered into your system. This can help prevent errors from happening in the first place and means that you don't have to write custom validation code for your forms and input fields.&lt;/p&gt;

&lt;p&gt;Zod can also help enforce rules around what data can be entered into your forms and input fields, ensuring that only valid data is accepted. By using Zod, you can avoid having to write any extra code or manually check user input against these rules - it will all be handled automatically by Zod. This makes your forms and inputs more reliable and robust against errors.&lt;/p&gt;

&lt;p&gt;Overall, using Zod can help improve the quality of your forms and input, making them easier to use, more accurate, and less prone to error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why should developers consider using Zod?
&lt;/h2&gt;

&lt;p&gt;Zod offers type-checking for JavaScript, which can be helpful for developers who want to ensure their code is error-free. Zod's validation methods can also be used to enforce input from users, ensuring that only valid data is submitted. Using Zod can help make your development process more efficient and reduce the risk of introducing bugs into your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get started with Zod
&lt;/h2&gt;

&lt;p&gt;First, you need to make sure you have strict mode enabled in your Typescript project.&lt;/p&gt;

&lt;p&gt;Then install the Zod package.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install zod 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Simple schemas
&lt;/h3&gt;

&lt;p&gt;Now you can create a schema to handle your needs, lets's say we want all inputs to be a string we can do that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";

// my schema should be a string

const mySchema = z.string();

mySchema.parse("Zod is awesome"); 
mySchema.parse(69) // throws an error because it is not a string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in some cases, we don't want to throw an error we want Zod to tell us we have had an invalid input so we can handle it somewhere else in the stack&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";


const mySchema = z.string();

mySchema.safeParse("Zod is still really cool");
// This will return { success: true: data: "Zod is still really cool"}


mySchema.safeParse(69)
// This will return { success: false: error: ZodError }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Object schemas
&lt;/h3&gt;

&lt;p&gt;Object schemas allow you to handle complex inputs and validate them and give you an inferred type.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";

const User = z.object({
  name: z.string(),
  username: z.string(),
  age: z.number(),
  email: z.string()
});

User.parse({ name: "James Perkins", username: "jamesperkins", age: 33, email: "contactme@jamesperkins.dev" });

// extract the inferred type
type User = z.infer&amp;lt;typeof User&amp;gt;;
// {name: string, username: string, age: number, email: string }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Built-in string validation
&lt;/h3&gt;

&lt;p&gt;One part no one wants to write is the regex to validate strings, I wrote a regex package a few years ago because who wants to remember how to validate the user input of an email? Zod provides a handful of validators, lets's improve our User object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";

const User = z.object({
  name: z.string(),
  username: z.string().min(5),
  age: z.number(),
  email: z.string().email()
});

User.parse({ name: "James Perkins", username: "jamesperkins", age: 33, email: "contactme@jamesperkins.dev" });

// extract the inferred type
type User = z.infer&amp;lt;typeof User&amp;gt;;
// {name: string, username: string, age: number, email: string }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a user has to input an email in our email field, which will be validated, and our username must be at least five characters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom error messages
&lt;/h3&gt;

&lt;p&gt;Zod allows you to provide a custom error to present to the user so they understand why they need to change input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { z } from "zod";

const User = z.object({
  name: z.string(),
  username: z.string().min(5, {message: "The username must be at least 5 characters"),
  age: z.number(),
  email: z.string().email({message: "The inputted email is invalid, please enter a valid email")
});

User.parse({ name: "James Perkins", username: "jamesperkins", age: 33, email: "contactme@jamesperkins.dev" });

// extract the inferred type
type User = z.infer&amp;lt;typeof User&amp;gt;;
// {name: string, username: string, age: number, email: string }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just the beginning of Zod, if you are interested, you can check out their &lt;a href="https://zod.dev/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>emptystring</category>
    </item>
    <item>
      <title>My top three tools (Mac edition)</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Sun, 31 Jul 2022 22:16:00 +0000</pubDate>
      <link>https://dev.to/perkinsjr/my-top-three-tools-mac-edition-hl0</link>
      <guid>https://dev.to/perkinsjr/my-top-three-tools-mac-edition-hl0</guid>
      <description>&lt;p&gt;I spend hours on my computer each day, whether it be writing, coding, or creating videos on YouTube. I have my work machine and personal MacBooks set up the same way, so I never have to worry about context switching. Below are the top three tools I use:&lt;/p&gt;

&lt;h2&gt;
  
  
  AltTab
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://alt-tab-macos.netlify.app/" rel="noopener noreferrer"&gt;AltTab&lt;/a&gt; is probably a dev tool you didn’t know you needed and once you have it, you wondered why you didn’t know about this earlier.&lt;/p&gt;

&lt;p&gt;AltTabbrings the power of Windows’s “alt-tab” window to Macs, if you have ever used a Mac one thing you will notice is there are zero previews when tabbing through your windows. This can be annoying when you have multiple Chrome windows or maybe two visual studio codes open as you can’t see which one to tab to.&lt;/p&gt;

&lt;p&gt;With AltTab you get to preview the window before you select it, and it can replace the default alt-tab experience. This is probably number one on my install list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Xnapper
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://xnapper.com/" rel="noopener noreferrer"&gt;Xnapper&lt;/a&gt; is a screenshot software developed by &lt;a href="https://twitter.com/tdinh_me" rel="noopener noreferrer"&gt;Tony Dinh&lt;/a&gt; it allows you to create awesome screenshots with minimal effort. It’s currently in beta but the developer rate is unreal. It gives you preset options for Twitter, YouTube thumbnails, Reddit, square, 16:9, and more.&lt;/p&gt;

&lt;p&gt;On top of this, you can have emails auto redacted, and padding and insets added. Add backgrounds, border-radius. Add a background and some shadow. A screenshot has never looked better!&lt;/p&gt;

&lt;p&gt;Below is an example!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1659304938%2FBlog%2520Posts%2Ftop-three-tools%2Fb242mweetlvbczxxvz21.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1659304938%2FBlog%2520Posts%2Ftop-three-tools%2Fb242mweetlvbczxxvz21.webp" alt="Example of a screenshot" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Warp
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://warp.dev" rel="noopener noreferrer"&gt;Warp&lt;/a&gt; is a terminal built on Rust. It’s super fast and has awesome features. &lt;a href="https://www.warp.dev/" rel="noopener noreferrer"&gt;Warp&lt;/a&gt;is a terminal that allows you to do a whole lot right from your keyboard. It gives you the ability to look through your history, and create workflows, and has IntelliSense. This is currently for Mac only but they are building one for Windows and one for Linux as well! So all the other users out there can have a great experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Command Palette
&lt;/h3&gt;

&lt;p&gt;The command palette allows you to search through all the different commands you can use within the Warp terminal. For example, create a new tab is &lt;code&gt;command + T&lt;/code&gt; or &lt;code&gt;command + ]&lt;/code&gt; is a new pane. The command pane is completely searchable so you can type in what you are looking for and hit Enter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Command History
&lt;/h3&gt;

&lt;p&gt;Command History is one of the best features of Warp, not because it’s a powerful or revolutionary feature, but because I and every developer have pressed the up arrow dozens of times to find that one command.&lt;/p&gt;

&lt;p&gt;You get two options when you press control + r you can scroll through all of the history items or you can just type at the beginning of the command. Here is an example of me typing the word yarn into my history search:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F705fnd8t4l17sc5p20p2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F705fnd8t4l17sc5p20p2.png" alt="History example" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Custom workflows&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.warp.dev/features/workflows" rel="noopener noreferrer"&gt;&lt;strong&gt;Custom workflows&lt;/strong&gt;&lt;/a&gt;are your alias on steroids, they are created using&lt;code&gt;**.yml**&lt;/code&gt;files and can be either user-specific or project specific.&lt;/p&gt;

&lt;p&gt;For user-specific ones you add them to&lt;code&gt;**~/.warp/workflows**&lt;/code&gt;and for project ones&lt;code&gt;**{{path\_to\_project}}/.warp/workflows**&lt;/code&gt;. The format is the same regardless, here is my&lt;code&gt;**code\_profile**&lt;/code&gt;one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Change code profiles
description: Change code profile for visual studio code
author: James Perkins
author_url: https://github.com/perkinsjr
tags: ['macos', 'shell', 'vscode']
shells:
    - zsh
    - bash
command: code --user-data-dir {{user_data_dir}} --extensions-dir {{extension_dir}}
arguments:
    - name: user_data_dir
      description: Directory of user-data
    - name: extension_dir
      description: Directory for extensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My custom workflow takes two arguments which and when used will open my &lt;code&gt;code_profiles&lt;/code&gt; stored for visual studio code.&lt;/p&gt;

&lt;p&gt;So that is it, my top three tools that I use every single day of the week, and absolutely love them!&lt;/p&gt;

</description>
      <category>tooling</category>
      <category>macbook</category>
      <category>devtools</category>
    </item>
    <item>
      <title>A/B testing with Next.js Middleware and TinaCMS</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Fri, 03 Jun 2022 12:56:16 +0000</pubDate>
      <link>https://dev.to/tinacms/ab-testing-with-nextjs-middleware-and-tinacms-cem</link>
      <guid>https://dev.to/tinacms/ab-testing-with-nextjs-middleware-and-tinacms-cem</guid>
      <description>&lt;p&gt;A/B testing can be an incredibly useful tool on any site. It allows you to increase user engagement, reduce bounce rates, increase conversion rate and effectively create content.&lt;/p&gt;

&lt;p&gt;Tina opens up the ability to A/B test, allowing marketing teams to test content without the need for the development team once it has been implemented.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;We are going to break this tutorial into two sections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Setting up A/B Tests with NextJS's middleware.&lt;/li&gt;
&lt;li&gt;Configuring our A/B Tests with Tina, so that our editors can spin up dynamic A/B Tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Creating our Tina application
&lt;/h2&gt;

&lt;p&gt;This blog post is going to use the Tailwind Starter. Using the &lt;code&gt;create-tina-app&lt;/code&gt; command, choose "Tailwind Starter" as the starter template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# create our Tina application

npx create-tina-app@latest a-b-testing

✔ Which package manager would you like to use? › Yarn
✔ What starter code would you like to use? › Tailwind Starter
Downloading files from repo tinacms/tina-cloud-starter. This might take a moment.
Installing packages. This might take a couple of minutes.

## Move into the directory and make sure everything is updated.

cd a-b-testing
yarn upgrade
yarn dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With your site running, you should be able to access it at &lt;code&gt;[http://localhost:3000](http://localhost:3000)&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Planning our tests
&lt;/h2&gt;

&lt;p&gt;The home page of the starter should look like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0o8wt0deymgd5wneg6f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi0o8wt0deymgd5wneg6f.png" alt="Tina Cloud Starter" width="800" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This page is already setup nicely for an A/B test, its page layout (&lt;code&gt;[slug].tsx&lt;/code&gt;) renders dynamic pages by accepting a variable &lt;code&gt;slug&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's start by creating an alternate version of the homepage called &lt;code&gt;home-b&lt;/code&gt;. You can do so in Tina &lt;a href="http://localhost:3000/admin#/collections/page/new" rel="noopener noreferrer"&gt;at http://localhost:3000/admin#/collections/page/new&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k84ceawccvtc2zi1lhe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2k84ceawccvtc2zi1lhe.png" alt="Tina Add document" width="800" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once that's done, go to: &lt;code&gt;http://localhost:3000/home-b&lt;/code&gt; to confirm that your new &lt;code&gt;/home-b&lt;/code&gt; page has been created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up our A/B tests
&lt;/h2&gt;

&lt;p&gt;Utlimately, we want our site to dynamically swap out certain pages for alternate page-variants, but we will first need a place to reference these active A/b tests.&lt;/p&gt;

&lt;p&gt;Let's create the following file at &lt;code&gt;content/ab-test/index.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "tests": [
    {
      "testId": "home",
      "href": "/",
      "variants": [
        {
          "testId": "b",
          "href": "/home-b"
        }
      ]
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll use this file later to tell our site that we have an A/B test on our homepage, using &lt;code&gt;/home-b&lt;/code&gt; as a variant.&lt;/p&gt;

&lt;p&gt;In the next step, we'll setup some NextJS middleware to dynamically use this page variant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering dynamic pages with NextJS middleware
&lt;/h2&gt;

&lt;p&gt;NextJS's middleware allows you to run code before the request is completed. We will leverage NextJS's middleware to conditionally swap out a page for its page-variant.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can learn more about NextJS's middleware &lt;a href="https://nextjs.org/docs/advanced-features/middleware" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Start by creating the &lt;code&gt;pages/_middleware.ts&lt;/code&gt; file, with the following code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//pages/_middleware.ts

import { NextRequest, NextResponse } from 'next/server'
import abTestDB from '../content/ab-test/index.json'
import { getBucket } from '../utils/getBucket'

// Check for AB tests on a given page
export function middleware(req: NextRequest) {
  // find the experiment that matches the request's url
  const matchingExperiment = abTestDB.tests.find(
    test =&amp;gt; test.href == req.nextUrl.pathname
  )

  if (!matchingExperiment) {
    // no matching A/B experiment found, so use the original page slug
    return NextResponse.next()
  }

  const COOKIE_NAME = `bucket-${matchingExperiment.testId}`
  const bucket = getBucket(matchingExperiment, req.cookies[COOKIE_NAME])

  const updatedUrl = req.nextUrl.clone()
  updatedUrl.pathname = bucket.url

  // Update the request URL to our bucket URL (if its changed)
  const res =
    req.nextUrl.pathname == bucket.url
      ? NextResponse.next()
      : NextResponse.rewrite(updatedUrl)

  // Add the bucket to cookies if it's not already there
  if (!req.cookies[COOKIE_NAME]) {
    res.cookie(COOKIE_NAME, bucket.id)
  }

  return res
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a little bit going on in the above snippet, but basically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We check if there's an active experiment for our request's URL&lt;/li&gt;
&lt;li&gt;If there is, we call &lt;code&gt;getBucket&lt;/code&gt; to see which version of the page we should deliver&lt;/li&gt;
&lt;li&gt;We update the user's cookies so that they consistently get delivered the same page for their given bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'll notice that the above code references a &lt;code&gt;getBucket&lt;/code&gt; function. We will need to create that, which will conditionally put us in a bucket for each page's A/B test.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// /utils/getBucket.ts

export const getBucket = (matchingABTest: any, bucketCookie?: string) =&amp;gt; {
  // if we already have been assigned a bucket, use that
  // otherwise, call getAssignedBucketId to put us in a bucket
  const bucketId =
    bucketCookie ||
    getAssignedBucketId([
      matchingABTest.testId,
      ...matchingABTest.variants.map(t =&amp;gt; t.testId),
    ])

  // check if our bucket matches a variant
  const matchingVariant = matchingABTest.variants.find(
    t =&amp;gt; t.testId == bucketId
  )

  if (matchingVariant) {
    // we matched a page variant
    return {
      url: matchingVariant.href,
      id: bucketId,
    }
  } else {
    //invalid bucket, or we're matched with the default AB test
    return {
      url: matchingABTest.href,
      id: matchingABTest.testId,
    }
  }
}

function getAssignedBucketId(buckets: readonly string[]) {
  // Get a random number between 0 and 1
  let n = cryptoRandom() * 100
  // Get the percentage of each bucket
  const percentage = 100 / buckets.length
  // Loop through the buckets and see if the random number falls
  // within the range of the bucket
  return (
    buckets.find(() =&amp;gt; {
      n -= percentage
      return n &amp;lt;= 0
    }) ?? buckets[0]
  )
}

function cryptoRandom() {
  return crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code snippet does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks if we're already in a bucket, and if not, it calls &lt;code&gt;getAssignedBucketId&lt;/code&gt; to randomly put us in a bucket.&lt;/li&gt;
&lt;li&gt;Returns the matching A/B test info for our given bucket.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That should be all for our middleware! Now, you should be able to visit the homepage at &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;, and you will have a 50-50 chance of being served the contents of &lt;code&gt;home.md&lt;/code&gt;, or &lt;code&gt;home-b.md&lt;/code&gt;. You can reset your bucket by clearing your browser's cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Tina to create A/B Tests
&lt;/h2&gt;

&lt;p&gt;At this point, our editors can edit the contents of both &lt;code&gt;home.md&lt;/code&gt; &amp;amp; &lt;code&gt;home-b.md&lt;/code&gt;, however we'd like our editors to be empowered to setup new A/B tests.&lt;/p&gt;

&lt;p&gt;Let's make our &lt;code&gt;content/ab-test/index.json&lt;/code&gt; file from earlier editable, by creating a Tina collection for it.&lt;/p&gt;

&lt;p&gt;Open up your &lt;code&gt;schema.ts&lt;/code&gt; and underneath the &lt;code&gt;Pages&lt;/code&gt; collection, create a new collection like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// ...,
    {
      label: "AB Test",
      name: "abtest",
      path: "content/ab-test",
      format: "json",
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now need to add the fields we want our content team to be able to edit, so that would be the ID, the page to run the test against, and the variants we want to run. We also want to be able to run tests on any number of pages so we will be using a list of objects for the &lt;code&gt;tests&lt;/code&gt; field.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to learn more about all the different field types and how to use them, check them out in our &lt;a href="https://tina.io/docs/schema/" rel="noopener noreferrer"&gt;Content Modeling documentation.&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    {
      label: "AB Test",
      name: "abtest",
      path: "content/ab-test",
      format: "json",
      fields: [
        {
          type: "object",
          label: "tests",
          name: "tests",
          list: true,
          ui: {
            itemProps: (item) =&amp;gt; {
              return { label: item.testId || "New A/B Test" };
            },
          },
          fields: [
            { type: "string", label: "Id", name: "testId" },
            {
              type: "string",
              label: "Page",
              name: "href",
              description:
                "This is the root page that will be conditionally swapped out",
            },
            {
              type: "object",
              name: "variants",
              label: "Variants",
              list: true,
              ui: {
                itemProps: (item) =&amp;gt; {
                  return { label: item.testId || "New variant" };
                },
              },
              fields: [
                { type: "string", label: "Id", name: "testId" },
                {
                  type: "string",
                  label: "Page",
                  name: "href",
                  description:
                    "This is the variant page that will be conditionally used instead of the original",
                },
              ],
            },
          ],
        },
      ],
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You may notice the &lt;code&gt;ui&lt;/code&gt; prop. We are using this to give a more descriptive label to the list items. You can read about this in our &lt;a href="https://tina.io/docs/extending-tina/customize-list-ui/" rel="noopener noreferrer"&gt;extending Tina documentation.&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We also need to update our &lt;code&gt;RouteMappingPlugin&lt;/code&gt; in &lt;code&gt;.tina/schema.ts&lt;/code&gt; to ensure that our collection is only editable with the basic editor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      const RouteMapping = new RouteMappingPlugin((collection, document) =&amp;gt; {
        if (collection.name == 'abtest') {
          return undefined
        }
        // ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, restart your dev server, and go to: &lt;code&gt;[http://localhost:3000/admin#/collections/abtest/index](http://localhost:3000/admin#/collections/abtest/index)&lt;/code&gt;. Your editors should be able to wire up their own A/B tests!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxz4ps4ztcdm310gih32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzxz4ps4ztcdm310gih32.png" alt="Tina Edit Variant" width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The process for editors to create new A/B tests would be as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Editor creates a new page in the CMS&lt;/li&gt;
&lt;li&gt;Editor wires up the page as a page-variant in the "A/B Tests" collection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that's it! We hope this empowers your team to start testing out different page variants to start optimizing your content!&lt;/p&gt;

&lt;h2&gt;
  
  
  How to keep up to date with Tina?
&lt;/h2&gt;

&lt;p&gt;The best way to keep up with Tina is to subscribe to our newsletter. We send out updates every two weeks. Updates include new features, what we have been working on, blog posts you may have missed, and more!&lt;/p&gt;

&lt;p&gt;You can subscribe by following this link and entering your email: &lt;a href="https://tina.io/community/" rel="noopener noreferrer"&gt;https://tina.io/community/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Tina Community Discord
&lt;/h3&gt;

&lt;p&gt;Tina has a community &lt;a href="https://discord.com/invite/zumN63Ybpf" rel="noopener noreferrer"&gt;Discord&lt;/a&gt; full of Jamstack lovers and Tina enthusiasts. When you join, you will find a place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;To get help with issues&lt;/li&gt;
&lt;li&gt;Find the latest Tina news and sneak previews&lt;/li&gt;
&lt;li&gt;Share your project with the Tina community, and talk about your experience&lt;/li&gt;
&lt;li&gt;Chat about the Jamstack&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tina Twitter
&lt;/h3&gt;

&lt;p&gt;Our Twitter account (&lt;a href="https://twitter.com/tina_cms" rel="noopener noreferrer"&gt;@tina_cms&lt;/a&gt;) announces the latest features, improvements, and sneak peeks to Tina. We would also be psyched if you tagged us in projects you have built.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>webdev</category>
      <category>cms</category>
      <category>middleware</category>
    </item>
    <item>
      <title>Using Next Middleware to access and use geolocation in a non dynamic route</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Thu, 05 May 2022 23:22:07 +0000</pubDate>
      <link>https://dev.to/perkinsjr/using-next-middleware-to-access-and-use-geolocation-in-a-non-dynamic-route-25n</link>
      <guid>https://dev.to/perkinsjr/using-next-middleware-to-access-and-use-geolocation-in-a-non-dynamic-route-25n</guid>
      <description>&lt;h1&gt;
  
  
  Using Next Middleware to access geolocation
&lt;/h1&gt;

&lt;p&gt;I don’t use Middleware from Next.js often, but I have dabbled a few times. Recently someone in another Developer’s Discord, I also have a Discord if you want to chat Jamstack, asked about how to add the Geolocation to a non-dynamic page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is &lt;code&gt;_middleware&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;Middleware allows you to use code over configuration. This gives you so much more flexibility over how your applications act when a user visits. The Middleware runs before the incoming request is completed, this allows you to modify the response by rewriting, redirecting, adding headers, or even streaming HTML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding Geolocation
&lt;/h2&gt;

&lt;p&gt;Depending on your situation, depends on where you need your &lt;code&gt;_middleware.(js.tsx)&lt;/code&gt; for this example it will run on every page. However, you can use it in nested routes to run for specific pages or dynamic routes.&lt;/p&gt;

&lt;p&gt;Create your file where you need it, inside that file you need to &lt;code&gt;import {NextResponse} from “next/server”&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then create &lt;code&gt;export async function middleware(req){}&lt;/code&gt; this is where we are going to be doing the work to add our user's location.&lt;/p&gt;

&lt;p&gt;The Middleware from Next.js has access to &lt;code&gt;cookies&lt;/code&gt;, &lt;code&gt;nextUrl&lt;/code&gt; , &lt;code&gt;i18n&lt;/code&gt; , &lt;code&gt;ua&lt;/code&gt;, &lt;code&gt;geo&lt;/code&gt;, &lt;code&gt;ip&lt;/code&gt; in the request which means we can use this to add geolocation to our application.&lt;/p&gt;

&lt;p&gt;To rewrite our request we are going to need to access the &lt;code&gt;nextUrl&lt;/code&gt; and &lt;code&gt;geo&lt;/code&gt; from our request.&lt;/p&gt;

&lt;p&gt;For example &lt;code&gt;const { nextUrl: url, geo } = req&lt;/code&gt; now we can modify the &lt;code&gt;nextUrl&lt;/code&gt; to add &lt;code&gt;searchParams&lt;/code&gt; (query parameters) to our request.&lt;/p&gt;

&lt;p&gt;Then finally return our new URL using &lt;code&gt;NextResponse&lt;/code&gt; below is the full request.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NextResponse } from 'next/server'

export async function middleware(req) {
  const { nextUrl: url, geo } = req
  const country = geo.country || 'US'

  url.searchParams.set('country', country)

  return NextResponse.rewrite(url)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Updating our page
&lt;/h3&gt;

&lt;p&gt;Now we need to add and use our geolocation to the page, to do that we need to first pass the &lt;code&gt;middleware&lt;/code&gt; through &lt;code&gt;getServerSideProps&lt;/code&gt; to pass it to the page. Currently, the &lt;code&gt;_middleware&lt;/code&gt; has all the info on the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// pass the _middleware to the index.js 

export const getServerSideProps = ({ query }) =&amp;gt; ({
  props: query,
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we can use it on the page, however, we want by passing the props to the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import { useRouter } from 'next/router'

// pass the _middleware to the index.js 

export const getServerSideProps = ({ query }) =&amp;gt; ({
  props: query,
})

export default function Home(props) {
    // use it right from the props
  console.log(props.country)
// or you can grab it from the router.
  const router = useRouter();

  console.log(router.query);
  return (
    &amp;lt;div className={styles.container}&amp;gt;
      &amp;lt;Head&amp;gt;
        &amp;lt;title&amp;gt;Create Next App&amp;lt;/title&amp;gt;
        &amp;lt;meta name="description" content="Generated by create next app" /&amp;gt;
        &amp;lt;link rel="icon" href="/favicon.ico" /&amp;gt;
      &amp;lt;/Head&amp;gt;

      &amp;lt;main className={styles.main}&amp;gt;
        &amp;lt;h1 className={styles.title}&amp;gt;
          Welcome to {props.country}
        &amp;lt;/h1&amp;gt;
      &amp;lt;/main&amp;gt;
    &amp;lt;/div&amp;gt;
  )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can handle whatever you need knowing the geolocation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One thing to note, geolocation made not be available so just be cautious, that is why we have a back up plan of &lt;code&gt;US&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>nextjs</category>
      <category>middleware</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Next.js - How to optimize fonts</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Mon, 02 May 2022 23:53:17 +0000</pubDate>
      <link>https://dev.to/perkinsjr/nextjs-how-to-optimize-fonts-4i7e</link>
      <guid>https://dev.to/perkinsjr/nextjs-how-to-optimize-fonts-4i7e</guid>
      <description>&lt;h1&gt;
  
  
  Next.js Font Optimized
&lt;/h1&gt;

&lt;p&gt;When using 3rd party fonts we have to make sure they are optimized, in the times before Next.js 10.2 you had to manually optimize them for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;link rel="preconnect" href="https://fonts.googleapis.com" /&amp;gt;
&amp;lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /&amp;gt;
&amp;lt;link href="https://fonts.googleapis.com/css2?family=Merriweather+Sans&amp;amp;display=swap" rel="stylesheet" /&amp;gt;
&amp;lt;link
   href="https://fonts.googleapis.com/css2?family=Squada+One:wght@400&amp;amp;display=swap"
   rel="stylesheet"
   /&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After 10.2 Next.js can now optimize Google fonts and Typekit with a variety of font optimization, inside your &lt;code&gt;_document.js&lt;/code&gt; you can now provide the font:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import Document, { Html, Head, Main, NextScript } from 'next/document'

class MyDocument extends Document {
  render() {
    return (
      &amp;lt;Html&amp;gt;
        &amp;lt;Head&amp;gt;
          &amp;lt;link
            href="https://fonts.googleapis.com/css2?family=Inter&amp;amp;display=optional"
            rel="stylesheet"
          /&amp;gt;
        &amp;lt;/Head&amp;gt;
        &amp;lt;body&amp;gt;
          &amp;lt;Main /&amp;gt;
          &amp;lt;NextScript /&amp;gt;
        &amp;lt;/body&amp;gt;
      &amp;lt;/Html&amp;gt;
    )
  }
}

export default MyDocument
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can even provide the preferred display options as a query parameter :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;block :&lt;/strong&gt; The text blocks (is invisible) for a short period&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;swap:&lt;/strong&gt; There is no block period (so no invisible text), and the text is shown immediately in the fallback font until the custom font loads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fallback&lt;/strong&gt;: This is somewhere in between block and swap. The text is invisible for a short period of time (100ms). Then if the custom font hasn't been downloaded, the text is shown in a fallback font (for about 3s), then swapped after the custom font loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;optional:&lt;/strong&gt; This behaves just like fallback, only the browser can decide to not use the custom font at all, based on the user's connection speed (if you're on a slow 3G or less, it will take forever to download the custom font and then swapping to it will be too late and extremely annoying)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;auto:&lt;/strong&gt; This basically ends up being the same as fallback.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>nextjs</category>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Warp is the future of terminals</title>
      <dc:creator>James Perkins</dc:creator>
      <pubDate>Sat, 16 Apr 2022 23:56:18 +0000</pubDate>
      <link>https://dev.to/perkinsjr/warp-is-the-future-of-terminals-24hh</link>
      <guid>https://dev.to/perkinsjr/warp-is-the-future-of-terminals-24hh</guid>
      <description>&lt;p&gt;I spend a lot of time working with tooling, whether it's for my job, content creation, or just development tools in general. Warp is a brand new terminal experience, so let's talk about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Warp?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.warp.dev/" rel="noopener noreferrer"&gt;Warp&lt;/a&gt; is a terminal that allows you to do a whole lot right from your keyboard. It gives you the ability to look through your history, create workflows and has intellisense. This is currently for Mac only but they are building one for Windows and one for Linux as well! So all the other users out there can have a great experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command Palette
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650150775%2FBlog%2520Posts%2FWarp%2Fn0wyavby3moe0hcqpgjf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650150775%2FBlog%2520Posts%2FWarp%2Fn0wyavby3moe0hcqpgjf.png" alt="Command Palette"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.warp.dev/features/command-palette" rel="noopener noreferrer"&gt;command palette&lt;/a&gt; allows you to search through all the different commands you can use within the Warp terminal. For example create new tab is &lt;code&gt;command + T&lt;/code&gt; or &lt;code&gt;command + ]&lt;/code&gt; is a new pane. The command pane is completely searchable so you can type in what you are looking for hit &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Command History
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650150937%2FBlog%2520Posts%2FWarp%2Fvdk9urrrz8wcvnp2tlaa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650150937%2FBlog%2520Posts%2FWarp%2Fvdk9urrrz8wcvnp2tlaa.png" alt="History Search"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.warp.dev/features/command-history" rel="noopener noreferrer"&gt;Command History&lt;/a&gt; is one of the &lt;em&gt;best&lt;/em&gt; features of Warp, not because it's powerful or revolutionary feature, but because I and every developer has press the up arrow dozens of times to find that &lt;em&gt;one&lt;/em&gt; command.&lt;/p&gt;

&lt;p&gt;You get two options when you press &lt;code&gt;control + r&lt;/code&gt; you can scroll through all of the history items or you can just type in the beginning of the command. Here is an example of me typing the word &lt;code&gt;yarn&lt;/code&gt; into my history search:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650151133%2FBlog%2520Posts%2FWarp%2Fx4baa9qeumyoyh1jbojm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650151133%2FBlog%2520Posts%2FWarp%2Fx4baa9qeumyoyh1jbojm.png" alt="History example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see I can now just select what I need from the list, speeding up the time spent remembering what did I type?&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflows
&lt;/h2&gt;

&lt;p&gt;Workflows are the powerhouse to this terminal application, workflows allow you to run commands similar to aliases. Each workflow gets a searchable title and description which make easy to find what you are looking for. The workflow pane can be opened by typing &lt;code&gt;shift + control + r&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you choose a workflow, you will prompted to fill in any arguments you might need to make the command work which you can navigate using &lt;code&gt;shift + tab&lt;/code&gt; :&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650151224%2FBlog%2520Posts%2FWarp%2Fqjhucpszhyfyuukoeqwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650151224%2FBlog%2520Posts%2FWarp%2Fqjhucpszhyfyuukoeqwj.png" alt="Command Example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom workflows
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.warp.dev/features/workflows" rel="noopener noreferrer"&gt;Custom workflows&lt;/a&gt; are your alias on steroids, they are created using &lt;code&gt;.yml&lt;/code&gt; files and can be either user specific or project specific.&lt;/p&gt;

&lt;p&gt;For user specific ones you add them to &lt;code&gt;~/.warp/workflows&lt;/code&gt; and for project ones &lt;code&gt;{{path_to_project}}/.warp/workflows&lt;/code&gt;. The format is the same regardless, here is my &lt;code&gt;code_profile&lt;/code&gt; one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Change code profiles
description: Change code profile for visual studio code
author: James Perkins
author_url: https://github.com/perkinsjr
tags: ['macos', 'shell', 'vscode']
shells:
    - zsh
    - bash
command: code --user-data-dir {{user_data_dir}} --extensions-dir {{extension_dir}}
arguments:
    - name: user_data_dir
      description: Directory of user-data
    - name: extension_dir
      description: Directory for extensions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My custom workflow takes two arguments which and when used will open my code_profiles have stored for visual studio code and it looks like this in the application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650151280%2FBlog%2520Posts%2FWarp%2Fnwmwrpfyez345dfazdwn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fres.cloudinary.com%2Fdub20ptvt%2Fimage%2Fupload%2Fv1650151280%2FBlog%2520Posts%2FWarp%2Fnwmwrpfyez345dfazdwn.png" alt="My work flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is just scratching the surface of the feature set that Warp offers and how it can be used. I recommend giving it a shot and seeing what you think.&lt;/p&gt;

</description>
      <category>terminals</category>
      <category>ssh</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
