<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Ravi Coding]]></title><description><![CDATA[Ravi Coding]]></description><link>https://blog.perfectbase.dev</link><generator>RSS for Node</generator><lastBuildDate>Tue, 14 Apr 2026 03:25:56 GMT</lastBuildDate><atom:link href="https://blog.perfectbase.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Super Fast Next.js App with the App Router]]></title><description><![CDATA[Some time ago I was building a new app with the Next.js App Router, but the app navigation felt quite slower than I was used to with the Pages Router, so I decided to try to understand better the App Router features and how they affect the navigation...]]></description><link>https://blog.perfectbase.dev/nextjs-navigation-performance</link><guid isPermaLink="true">https://blog.perfectbase.dev/nextjs-navigation-performance</guid><category><![CDATA[Next.js]]></category><category><![CDATA[performance]]></category><category><![CDATA[app router]]></category><dc:creator><![CDATA[Ravi]]></dc:creator><pubDate>Fri, 20 Dec 2024 13:11:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734698851620/49931e54-f249-4578-b6d6-4b901fa1ad0b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Some time ago I was building a new app with the Next.js App Router, but the app navigation felt quite slower than I was used to with the Pages Router, so I decided to try to understand better the App Router features and how they affect the navigation speed.</p>
<h2 id="heading-demo">Demo</h2>
<p>I was making some example pages for testing and decided to make a full demo app to showcase each feature and how they affect navigation performance.</p>
<p><a target="_blank" href="https://nextperformance.vercel.app">Demo App</a></p>
<p><a target="_blank" href="https://nextperformance.vercel.app"><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734590864563/6a2a799f-d256-4882-8ead-ce5ddf60dd9d.png" alt class="image--center mx-auto" /></a></p>
<h2 id="heading-app-router-vs-pages-router">App Router vs Pages Router</h2>
<p>When you compare the performance of the Pages Router with the App Router, you will notice that by default the Pages Router navigation is faster, while the App Router initial page load is faster (although it is hard to notice the initial page load speed difference without some tool to measure). That happens mainly because of 2 things. With the Pages Router, the necessary JavaScript to navigate to another page is already downloaded to the browser on the page's initial load. With the App Router, the javascript downloaded to the browser is usually smaller and the dynamic content is streamed through the same initial page request (while you would have 2 separate requests for the Pages Router).</p>
<h2 id="heading-displaying-dynamic-content">Displaying Dynamic Content</h2>
<p>Initially I thought that I would get a fast navigation just by wrapping the dynamic content with <code>Suspense</code> with a fallback for a loading state. But as you can see from <a target="_blank" href="https://nextperformance.vercel.app/app-router/suspense">the example</a>, that doesn't happen and the navigation still feels kind of slow.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Page</span>(<span class="hljs-params">props: { params: <span class="hljs-built_in">Promise</span>&lt;{ id: <span class="hljs-built_in">string</span> }&gt; }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      Static Title
      &lt;Suspense fallback={&lt;Spinner /&gt;}&gt;
        &lt;DynamicComponent {...props} /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>If we add a <code>loading.tsx</code> to this route segment, we finally achieve a fast navigation. However, this introduces a new issue with the loading cascade: first, the loading from <code>loading.tsx</code> is displayed, and then the loading from the <code>Suspense</code> fallback gets displayed.</p>
<p>We can also make the navigation faster by using the <code>edge</code> runtime. All we need to do is add <code>export const runtime = 'edge';</code> in our page code and the page load should be faster. That said, the <code>edge</code> runtime does not support all the node.js features and some libraries might not work as expected.</p>
<h2 id="heading-caching">Caching</h2>
<h4 id="heading-client-side">Client Side</h4>
<p>By enabling the client side router cache, we can get a fast navigation on the second time the user visits a certain page. This was the default behavior on Next.js 14, but it changed with Next.js 15. Now if we want to enable this behaviour we need to add the following in the <code>next.config.ts</code> file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> nextConfig: NextConfig = {
  experimental: {
    staleTimes: {
      dynamic: <span class="hljs-number">30</span>,
      <span class="hljs-keyword">static</span>: <span class="hljs-number">180</span>,
    },
  },
};
</code></pre>
<p>This will make Next.js cache dynamic visited pages on the client side for 30 seconds. That said, the cache is only for that specific user and it goes away when you reload the page. You can also clear the client side cache programmatically by calling <code>router.refresh()</code>.</p>
<h4 id="heading-server-side">Server Side</h4>
<p>We can also cache the dynamic content on the server side. This won't actually affect the app's navigation speed when compared with wrapping it with <code>Suspense</code>, but it is faster because there will be no loading state.</p>
<p>On the demo I used <code>unstable_cache()</code>, but you can also use the newer experimental <code>use cache</code> directive, which is probably what people will be using in production once it becomes stable.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> getCachedDynamicData = unstable_cache(getDynamicData);
</code></pre>
<h2 id="heading-partial-prerendering-ppr">Partial Prerendering (PPR)</h2>
<p>Enabling PPR on our app makes it possible to have static content and dynamic content on the same page. Where the pre-rendered static shell is shown instantly while the dynamic content wrapped with <code>Suspense</code> loads asynchronously.</p>
<p>For now we need to be on the latest canary version of Next.js to enable PPR, but hopefully soon it will come to the stable release. When that happens we might be able to build Next.js apps without thinking too much about navigation performance, as wrapping the dynamic part with <code>Suspense</code> will be enough for fast navigation.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> nextConfig: NextConfig = {
  experimental: {
    ppr: <span class="hljs-literal">true</span>,
  },
};
</code></pre>
<h2 id="heading-prefetching">Prefetching</h2>
<p>By default, Next.js already prefetches static content of links that are visible in the screen. If we want to go one step further, we can set <code>prefetch={true}</code> in our <code>Link</code> components. This will make Next.js prefetch the whole page, including the dynamic parts, making navigation instant and without loading even for dynamic pages.</p>
<pre><code class="lang-typescript">&lt;Link href={link} prefetch&gt;Link&lt;/Link&gt;
</code></pre>
<p>Of course, this will increase the number of unnecessary requests, which might increase the bandwidth usage on the hosting platform and also cause some unnecessary load on the server.</p>
<p>The server-side load can be avoided if the content can be cached reducing the need for server calls to databases or external services.</p>
<h2 id="heading-other-tricks">Other Tricks</h2>
<p>In <a target="_blank" href="https://nextperformance.vercel.app/app-router/almighty">this demo</a> you can see how fast you can make your app.</p>
<p>In addition to server-side caching and prefetching, this demo also uses two other tricks to make the navigation even faster. The first one is prefetching target page's images on link hover. The other is to navigate <code>onMouseDown</code> (as soon as you click instead of waiting for release).</p>
<p>These tricks were taken from the <a target="_blank" href="https://github.com/ethanniser/NextFaster">NextFaster</a> project. The main code you will want to check is the <code>prefetch-images</code> api route and the custom <code>link.tsx</code> component.</p>
<p>They also have a cost breakdown in the README file of this experimental project, which you might want to check.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Right now we still have to do a little bit of work to make our Next.js app navigation fast with the App Router. Hopefully we won't need to think too much about it after PPR becomes stable. That said, the App Router offers many nice features like the ability to directly call backend code from server components, server actions, caching, layouts, and more. So personally I will be using the App Router for my next projects.</p>
<h2 id="heading-thanks-for-reading">Thanks for reading!</h2>
<p>👋 Here are my links!</p>
<ul>
<li><p><a target="_blank" href="https://youtube.com/@perfectbase">YouTube</a></p>
</li>
<li><p><a target="_blank" href="https://x.com/RaviCoding">X</a></p>
</li>
<li><p><a target="_blank" href="https://bsky.app/profile/ravicoding.bsky.social">Bluesky</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building an AI Script to Generate Mock Data with Realistic Images]]></title><description><![CDATA[Generating mock data for an app can be very time-consuming, even if you use AI chatbots. You have to write out prompts, copy the results, and then somehow import everything into your database. It’s manageable for a few items, but what if you need doz...]]></description><link>https://blog.perfectbase.dev/building-an-ai-script-to-generate-mock-data-with-realistic-images</link><guid isPermaLink="true">https://blog.perfectbase.dev/building-an-ai-script-to-generate-mock-data-with-realistic-images</guid><category><![CDATA[openai]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[edgestore]]></category><category><![CDATA[dalle]]></category><dc:creator><![CDATA[Ravi]]></dc:creator><pubDate>Sat, 23 Nov 2024 04:00:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732333125282/c496fe78-254c-4015-a14b-78d931ca4a93.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Generating mock data for an app can be very time-consuming, even if you use AI chatbots. You have to write out prompts, copy the results, and then somehow import everything into your database. It’s manageable for a few items, but what if you need dozens or even hundreds? That’s the issue I faced while working on a new project. I needed lots of mock items, and doing it manually simply wasn’t practical. So, I decided to automate the entire process by writing a script that uses OpenAI’s API. Here’s how I did it.</p>
<h2 id="heading-features-of-the-script">Features of the Script</h2>
<ul>
<li><p><strong>⭐ JSON Structured Output</strong>: Uses OpenAI's GPT-4o-mini model to generate structured JSON output.</p>
</li>
<li><p><strong>⭐ Configurable Batch Sizes</strong>: You can control how quickly the data is generated.</p>
</li>
<li><p><strong>⭐ Image Generation</strong>: Generates realistic images for items using DALL·E 3.</p>
</li>
<li><p><strong>⭐ Image Optimization</strong>: Compresses images into WebP format using Sharp for better performance.</p>
</li>
<li><p><strong>⭐ Image Upload</strong>: Stores the optimized images in <a target="_blank" href="https://edgestore.dev/">Edge Store</a> for easy access.</p>
</li>
<li><p><strong>⭐ Database Integration</strong>: Seamlessly inserts the generated data into your database.</p>
</li>
<li><p><strong>⭐ Reusable Design</strong>: Can be easily adapted for different kinds of mock data.</p>
</li>
</ul>
<h2 id="heading-running-standalone-typescript-scripts">Running Standalone TypeScript Scripts</h2>
<p>The first thing I needed was a way to run scripts in my app, with access to all necessary environment variables. I used <code>tsx</code> and <code>dotenv-cli</code> for this.</p>
<p>Install them using:</p>
<pre><code class="lang-bash">npm i -D tsx dotenv-cli
</code></pre>
<p>Now, we can create a simple script:</p>
<pre><code class="lang-typescript">(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"TEST"</span>);
})();
</code></pre>
<p>And run it with:</p>
<pre><code class="lang-bash">npx dotenv -e .env tsx ./src/scripts/test.ts
</code></pre>
<h2 id="heading-generating-mock-data">Generating Mock Data</h2>
<p>I wanted to generate a list of recipes with specific fields in JSON format to add to my database. When I first ran multiple generations in parallel, I got some duplicate recipes. To avoid that, I switched to generating them sequentially and included the existing recipes in the prompt to minimize duplication. There are definitely ways to implement seeding logic for parallel generation without duplicates, but for this purpose, I was fine with generating them in series.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { db } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/server/db"</span>;
<span class="hljs-keyword">import</span> OpenAI <span class="hljs-keyword">from</span> <span class="hljs-string">"openai"</span>;
<span class="hljs-keyword">import</span> { zodResponseFormat } <span class="hljs-keyword">from</span> <span class="hljs-string">"openai/helpers/zod"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;

<span class="hljs-comment">// Schema for the recipe data returned by OpenAI</span>
<span class="hljs-keyword">const</span> recipeSchema = z.object({
  recipes: z.array(
    z.object({
      name: z.string(),
      description: z.string(),
      ingredients: z.array(z.string()),
      steps: z.array(
        z.object({
          instruction: z.string(),
        })
      ),
      duration: z.number(),
      servings: z.number(),
    })
  ),
});

<span class="hljs-keyword">type</span> Recipe = z.infer&lt;<span class="hljs-keyword">typeof</span> recipeSchema&gt;[<span class="hljs-string">"recipes"</span>][<span class="hljs-built_in">number</span>] &amp; {
  image?: <span class="hljs-built_in">string</span>; <span class="hljs-comment">// This will be populated later</span>
};

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateRecipes</span>(<span class="hljs-params">count: <span class="hljs-built_in">number</span>, allRecipeNamesStr: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> prompt = <span class="hljs-string">`Generate <span class="hljs-subst">${count}</span> unique and diverse recipes that are different from the following recipes: <span class="hljs-subst">${allRecipeNamesStr}</span>.`</span>;

  <span class="hljs-comment">// Generate a list of recipes in JSON format using OpenAI's API</span>
  <span class="hljs-keyword">const</span> completion = <span class="hljs-keyword">await</span> openai.chat.completions.create({
    model: <span class="hljs-string">"gpt-4o-mini"</span>,
    messages: [{ role: <span class="hljs-string">"user"</span>, content: prompt }],
    response_format: zodResponseFormat(recipeSchema, <span class="hljs-string">"recipes"</span>),
  });

  <span class="hljs-keyword">const</span> responseContent = completion.choices[<span class="hljs-number">0</span>]?.message?.content ?? <span class="hljs-string">""</span>;
  <span class="hljs-keyword">return</span> recipeSchema.parse(<span class="hljs-built_in">JSON</span>.parse(responseContent)).recipes;
}
</code></pre>
<h2 id="heading-generating-images">Generating Images</h2>
<p>To generate images, I used the <code>dall-e-3</code> model. The cost is $0.04 per image. You could opt for <code>dall-e-2</code> at half the cost, but the images aren’t as good.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateRecipeImage</span>(<span class="hljs-params">recipe: Recipe</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Generating image for <span class="hljs-subst">${recipe.name}</span>`</span>);

  <span class="hljs-keyword">const</span> imagePrompt = <span class="hljs-string">`An appetizing photo of the dish: <span class="hljs-subst">${recipe.name}</span>`</span>;

  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> openai.images.generate({
    model: <span class="hljs-string">"dall-e-3"</span>,
    prompt: imagePrompt,
    size: <span class="hljs-string">"1024x1024"</span>,
  });

  <span class="hljs-keyword">const</span> url = res.data[<span class="hljs-number">0</span>]?.url;
  <span class="hljs-keyword">if</span> (!url) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"No image url found"</span>);
  }
  <span class="hljs-keyword">return</span> url;
}
</code></pre>
<h2 id="heading-optimizing-the-image">Optimizing the Image</h2>
<p>The images generated by DALL-E were quite large, so I used Sharp to convert them to WebP. This reduced the size from about 1.7MB to 170KB, without noticeable loss of quality.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> sharp <span class="hljs-keyword">from</span> <span class="hljs-string">"sharp"</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">optimizeImage</span>(<span class="hljs-params">imageUrl: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(imageUrl);
  <span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to fetch image: <span class="hljs-subst">${response.statusText}</span>`</span>);
  }

  <span class="hljs-keyword">const</span> arrayBuffer = <span class="hljs-keyword">await</span> response.arrayBuffer();

  <span class="hljs-keyword">const</span> optimizedBuffer = <span class="hljs-keyword">await</span> sharp(Buffer.from(arrayBuffer))
    .webp() <span class="hljs-comment">// Default quality is 80</span>
    .toBuffer();

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Blob([optimizedBuffer], { <span class="hljs-keyword">type</span>: <span class="hljs-string">"image/webp"</span> });
}
</code></pre>
<h2 id="heading-uploading-the-image">Uploading the Image</h2>
<p>The images from DALL-E are hosted on URLs that expire, so I needed to upload them to my storage. Naturally, I used <a target="_blank" href="https://edgestore.dev/">Edge Store</a> for this, which made storing the images easy and free.</p>
<ul>
<li>Set up the Edge Store project keys in your environment variables:</li>
</ul>
<pre><code class="lang-plaintext">EDGE_STORE_ACCESS_KEY=xxx
EDGE_STORE_SECRET_KEY=xxx
</code></pre>
<ul>
<li>Configure the bucket for the images:</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { initEdgeStore } <span class="hljs-keyword">from</span> <span class="hljs-string">"@edgestore/server"</span>;

<span class="hljs-keyword">const</span> es = initEdgeStore.create();
<span class="hljs-keyword">const</span> edgeStoreRouter = es.router({
  img: es.imageBucket(), <span class="hljs-comment">// A simple public image bucket</span>
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> backendClient = initEdgeStoreClient({
  router: edgeStoreRouter,
});
</code></pre>
<ul>
<li>Upload the image blob:</li>
</ul>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">uploadImage</span>(<span class="hljs-params">blob: Blob</span>) </span>{
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> backendClient.img.upload({
    content: {
      blob,
      extension: <span class="hljs-string">"webp"</span>,
    },
  });
  <span class="hljs-keyword">return</span> res.url;
}
</code></pre>
<p>P.S. I'm the creator of Edge Store, so I'm obviously biased towards it. 😇</p>
<h2 id="heading-inserting-data-into-the-database">Inserting Data into the Database</h2>
<p>Once I had all the data and images ready, I inserted them into my database. I used <a target="_blank" href="https://orm.drizzle.team/">Drizzle</a> with Postgres on <a target="_blank" href="https://supabase.com/">Supabase</a>, but this approach works with any database technology you prefer.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">insertRecipesIntoDB</span>(<span class="hljs-params">recipes: Recipe[]</span>) </span>{
  <span class="hljs-keyword">await</span> db
    .insert(tRecipe)
    .values(
      recipes.map(<span class="hljs-function">(<span class="hljs-params">recipe</span>) =&gt;</span> ({
        name: recipe.name,
        description: recipe.description,
        ingredients: recipe.ingredients,
        steps: recipe.steps,
        duration: recipe.duration,
        servings: recipe.servings,
        image: recipe.image,
      }))
    )
    .execute();
}
</code></pre>
<h2 id="heading-cost-considerations">Cost Considerations</h2>
<p>The cost of text generation is negligible compared to image generation, which costs $0.04 per image ($0.02 if using <code>dall-e-2</code>). For example, generating 30 items costs about $1.20.</p>
<h2 id="heading-full-code">Full Code</h2>
<p>Here's the complete code that brings everything together.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { db } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/server/db"</span>;
<span class="hljs-keyword">import</span> { tRecipe } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/server/db/schema"</span>;
<span class="hljs-keyword">import</span> OpenAI <span class="hljs-keyword">from</span> <span class="hljs-string">"openai"</span>;
<span class="hljs-keyword">import</span> { zodResponseFormat } <span class="hljs-keyword">from</span> <span class="hljs-string">"openai/helpers/zod"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"zod"</span>;
<span class="hljs-keyword">import</span> { backendClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@/lib/edgestore"</span>;
<span class="hljs-keyword">import</span> sharp <span class="hljs-keyword">from</span> <span class="hljs-string">"sharp"</span>;

<span class="hljs-keyword">const</span> openai = <span class="hljs-keyword">new</span> OpenAI();

<span class="hljs-comment">// Schema for the recipe data returned by OpenAI</span>
<span class="hljs-keyword">const</span> recipeSchema = z.object({
  recipes: z.array(
    z.object({
      name: z.string(),
      description: z.string(),
      ingredients: z.array(z.string()),
      steps: z.array(
        z.object({
          instruction: z.string(),
        })
      ),
      duration: z.number(),
      servings: z.number(),
    })
  ),
});

<span class="hljs-keyword">type</span> Recipe = z.infer&lt;<span class="hljs-keyword">typeof</span> recipeSchema&gt;[<span class="hljs-string">"recipes"</span>][<span class="hljs-built_in">number</span>] &amp; {
  image?: <span class="hljs-built_in">string</span>; <span class="hljs-comment">// Add an image field that will be populated on a future prompt</span>
};

(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> totalRecipes = <span class="hljs-number">20</span>; <span class="hljs-comment">// Total number of recipes to generate</span>
  <span class="hljs-keyword">const</span> recipesPerBatch = <span class="hljs-number">10</span>; <span class="hljs-comment">// Number of recipes to generate per batch</span>
  <span class="hljs-keyword">const</span> totalBatches = <span class="hljs-built_in">Math</span>.ceil(totalRecipes / recipesPerBatch); <span class="hljs-comment">// Calculate the total number of batches</span>

  <span class="hljs-comment">// Fetch existing recipe names from the database to avoid duplicates</span>
  <span class="hljs-keyword">const</span> allRecipeNames = (<span class="hljs-keyword">await</span> db.query.tRecipe.findMany().execute()).map(
    <span class="hljs-function">(<span class="hljs-params">recipe</span>) =&gt;</span> recipe.name
  );

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> batchNumber = <span class="hljs-number">1</span>; batchNumber &lt;= totalBatches; batchNumber++) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Generating recipes batch <span class="hljs-subst">${batchNumber}</span>/<span class="hljs-subst">${totalBatches}</span>`</span>);

    <span class="hljs-comment">// Generate a batch of new recipes, ensuring they are unique</span>
    <span class="hljs-keyword">const</span> recipes = <span class="hljs-keyword">await</span> generateRecipes(
      recipesPerBatch,
      allRecipeNames.join(<span class="hljs-string">", "</span>)
    );

    <span class="hljs-comment">// Generate images for each recipe in the batch</span>
    <span class="hljs-keyword">await</span> generateImagesForRecipes(recipes);

    <span class="hljs-comment">// Insert the new recipes into the database</span>
    <span class="hljs-keyword">await</span> insertRecipesIntoDB(recipes);

    <span class="hljs-comment">// Update the list of all recipe names with the newly added recipes</span>
    allRecipeNames.push(...recipes.map(<span class="hljs-function">(<span class="hljs-params">recipe</span>) =&gt;</span> recipe.name));
  }
})();

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateRecipes</span>(<span class="hljs-params">count: <span class="hljs-built_in">number</span>, allRecipeNamesStr: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> prompt = <span class="hljs-string">`Generate <span class="hljs-subst">${count}</span> unique and diverse recipes that are different from the following recipes: <span class="hljs-subst">${allRecipeNamesStr}</span>.`</span>;

  <span class="hljs-comment">// Generate a list of recipes in JSON format using OpenAI's API</span>
  <span class="hljs-keyword">const</span> completion = <span class="hljs-keyword">await</span> openai.chat.completions.create({
    model: <span class="hljs-string">"gpt-4o-mini"</span>,
    messages: [{ role: <span class="hljs-string">"user"</span>, content: prompt }],
    response_format: zodResponseFormat(recipeSchema, <span class="hljs-string">"recipes"</span>),
  });

  <span class="hljs-comment">// Extract the content from the AI's response</span>
  <span class="hljs-keyword">const</span> responseContent = completion.choices[<span class="hljs-number">0</span>]?.message?.content ?? <span class="hljs-string">""</span>;

  <span class="hljs-comment">// Parse and validate the response using the defined schema</span>
  <span class="hljs-keyword">const</span> generatedRecipes = recipeSchema.parse(
    <span class="hljs-built_in">JSON</span>.parse(responseContent)
  ).recipes;

  <span class="hljs-keyword">return</span> generatedRecipes;
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateImagesForRecipes</span>(<span class="hljs-params">recipes: Recipe[]</span>) </span>{
  <span class="hljs-keyword">const</span> batchSize = <span class="hljs-number">5</span>; <span class="hljs-comment">// Number of images to generate concurrently</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; recipes.length; i += batchSize) {
    <span class="hljs-keyword">const</span> batch = recipes.slice(i, i + batchSize);
    <span class="hljs-comment">// Generate images for the current batch concurrently</span>
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
      batch.map(<span class="hljs-keyword">async</span> (recipe) =&gt; {
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">const</span> imageUrl = <span class="hljs-keyword">await</span> generateRecipeImage(recipe);
          recipe.image = imageUrl; <span class="hljs-comment">// Add the image URL to the recipe</span>
        } <span class="hljs-keyword">catch</span> (error) {
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Failed to generate image for <span class="hljs-subst">${recipe.name}</span>:`</span>, error);
        }
      })
    );

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Waiting before the next batch...`</span>);
    <span class="hljs-comment">// The tier1 rate limit is 5 requests per minute 😢</span>
    <span class="hljs-comment">// You might be able to remove this delay if you have a higher tier</span>
    <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve</span>) =&gt;</span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">50000</span>)); <span class="hljs-comment">// Wait for 50 seconds</span>
  }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">generateRecipeImage</span>(<span class="hljs-params">recipe: Recipe</span>) </span>{
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Generating image for <span class="hljs-subst">${recipe.name}</span>`</span>);

  <span class="hljs-keyword">const</span> imagePrompt = <span class="hljs-string">`An appetizing photo of the dish: <span class="hljs-subst">${recipe.name}</span>`</span>;

  <span class="hljs-comment">// Call OpenAI's image generation API with the prompt</span>
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> openai.images.generate({
    model: <span class="hljs-string">"dall-e-3"</span>,
    prompt: imagePrompt,
    size: <span class="hljs-string">"1024x1024"</span>,
  });

  <span class="hljs-comment">// Extract the image URL from the response</span>
  <span class="hljs-keyword">const</span> url = res.data[<span class="hljs-number">0</span>]?.url;
  <span class="hljs-keyword">if</span> (!url) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"No image url found"</span>);
  }

  <span class="hljs-comment">// Optimize the image before uploading</span>
  <span class="hljs-keyword">const</span> blob = <span class="hljs-keyword">await</span> optimizeImage(url);

  <span class="hljs-comment">// Upload the optimized image to edgestore</span>
  <span class="hljs-keyword">const</span> esRes = <span class="hljs-keyword">await</span> backendClient.img.upload({
    content: {
      blob,
      extension: <span class="hljs-string">"webp"</span>,
    },
  });

  <span class="hljs-keyword">return</span> esRes.url;
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">optimizeImage</span>(<span class="hljs-params">imageUrl: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-comment">// Fetch the image from the provided URL</span>
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(imageUrl);
  <span class="hljs-keyword">if</span> (!response.ok) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to fetch image: <span class="hljs-subst">${response.statusText}</span>`</span>);
  }

  <span class="hljs-comment">// Convert the response to an ArrayBuffer for processing</span>
  <span class="hljs-keyword">const</span> arrayBuffer = <span class="hljs-keyword">await</span> response.arrayBuffer();

  <span class="hljs-comment">// Use sharp to convert the image to WebP format</span>
  <span class="hljs-keyword">const</span> optimizedBuffer = <span class="hljs-keyword">await</span> sharp(Buffer.from(arrayBuffer))
    .webp() <span class="hljs-comment">// Default quality is 80</span>
    .toBuffer();

  <span class="hljs-comment">// Create a Blob from the optimized buffer</span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Blob([optimizedBuffer], { <span class="hljs-keyword">type</span>: <span class="hljs-string">"image/webp"</span> });
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">insertRecipesIntoDB</span>(<span class="hljs-params">recipes: Recipe[]</span>) </span>{
  <span class="hljs-keyword">await</span> db
    .insert(tRecipe)
    .values(
      recipes.map(<span class="hljs-function">(<span class="hljs-params">recipe</span>) =&gt;</span> ({
        name: recipe.name,
        description: recipe.description,
        ingredients: recipe.ingredients,
        steps: recipe.steps,
        duration: recipe.duration,
        servings: recipe.servings,
        image: recipe.image,
      }))
    )
    .execute();
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Automating the creation of mock data with AI saved me a lot of time, especially when compared to manually generating and managing data. This approach scales well, whether you need a handful of items or hundreds. Plus, having realistic data adds to the overall presentation of your app, making it feel more complete. Although it requires some time investment initially, having the logic ready now means that I'll be able to do it much faster in future projects. If you’re in need of lots of mock data, you might want to try this approach!</p>
<h2 id="heading-thanks-for-reading">Thanks for reading!</h2>
<p>👋 Here are my links!</p>
<ul>
<li><p><a target="_blank" href="https://youtube.com/@perfectbase">YouTube</a></p>
</li>
<li><p><a target="_blank" href="https://x.com/RaviCoding">X</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building a SaaS for storing and handling images in your app. Is it a good idea?]]></title><description><![CDATA[Building side projects can be a daunting task, especially when you’re short on time and resources. Luckily, there are some awesome tools out there to help us out - Vercel for hosting and PlanetScale for database, for example. I love to leverage these...]]></description><link>https://blog.perfectbase.dev/edge-store-idea-validation</link><guid isPermaLink="true">https://blog.perfectbase.dev/edge-store-idea-validation</guid><category><![CDATA[SaaS]]></category><category><![CDATA[Startups]]></category><category><![CDATA[ideas]]></category><category><![CDATA[Build In Public]]></category><dc:creator><![CDATA[Ravi]]></dc:creator><pubDate>Thu, 01 Dec 2022 00:12:59 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1669853039850/hq6qQeIQV.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building side projects can be a daunting task, especially when you’re short on time and resources. Luckily, there are some awesome tools out there to help us out - Vercel for hosting and PlanetScale for database, for example. I love to leverage these services for my projects, but there is one thing that most of my projects require, and it’s always a pain to set up: handling images. I saw this as an opportunity to build it myself, and here I'm going to share the idea and what I'm doing to validate it. I would love to have your opinion.</p>
<h2 id="heading-current-solution">Current solution</h2>
<p>Let's start with the basics. To use my current process, I need to ① have an AWS account. This requires a credit card, which can be a pain for some users. Then, ② I need to set up a private S3 bucket and ③ link it to a CloudFront distribution, along with setting up all the necessary permissions. Finally, ④ I use presigned URLs generated from my API with the logic of who has access to what.</p>
<p>All in all, this current process works for me, but I'm always on the lookout for an easier, more efficient way of doing things. So I'm wondering - how are other people handling it? Are there any alternative methods out there that I might have missed? If anyone has any ideas or suggestions, I'd love to hear them in the comments section below.</p>
<p>Thanks in advance for any input you might have!</p>
<h2 id="heading-the-idea">The idea</h2>
<p>Here's the idea I have. I've been calling it "<a target="_blank" href="https://edge-store.com/">Edge Store</a>".</p>
<p>With Edge Store, you would be able to:</p>
<ul>
<li><p>create a free account and get enough storage for at least 2 or 3 small projects.</p>
</li>
<li><p>use the SDK to abstract the logic for handling image access and uploads.</p>
</li>
<li><p>configure a JWT-based access control.</p>
</li>
<li><p>enjoy the great performance on accessing the image from anywhere in the world by leveraging the AWS edge infrastructure.</p>
</li>
</ul>
<p>You wouldn't need to worry about scalability and capacity in the paid plan, as Edge Store would use a pay-as-you-grow model. Additionally, here are some other features that I believe would be nice to have:</p>
<ul>
<li><p>Use the service web app to batch upload images</p>
</li>
<li><p>Batch resize images</p>
</li>
<li><p>Change extensions</p>
</li>
<li><p>Automatically upload a small version of the image</p>
</li>
</ul>
<p>I'd love to hear your thoughts on this idea so let me know what you think.</p>
<h2 id="heading-sdk-example">SDK Example</h2>
<h4 id="heading-client">Client</h4>
<p>To set up the client SDK, you would use your service public key, and optionally you could pass the JWT cookie name if you wish to control who can access and upload each image.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { EdgeStore } <span class="hljs-keyword">from</span> <span class="hljs-string">"@edge-store/client"</span>;

<span class="hljs-keyword">const</span> edgeStore = <span class="hljs-keyword">new</span> EdgeStore({
  publicKey: <span class="hljs-string">"your-public-key"</span>,
  jwtCookie: <span class="hljs-string">"your-jwt-cookie-name"</span>,
});
</code></pre>
<p>The following code is to upload an image. Under the hood, it would check the JWT with your configuration, generate the presigned URL, and then upload the image.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> edgeStore.uploadImage({
  file: file,
  name: <span class="hljs-string">"image.jpg"</span>,
  <span class="hljs-comment">// Optionally you could resize your images on the fly.</span>
  width: <span class="hljs-number">300</span>,
  height: <span class="hljs-number">300</span>,
});
</code></pre>
<p>And you can also use the SDK to get the image URL:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> src = <span class="hljs-keyword">await</span> edgeStore.getImageSrc({
  name: <span class="hljs-string">"image.jpg"</span>,
  <span class="hljs-comment">// Optional: images can be resized on the edge</span>
  width: <span class="hljs-number">100</span>,
  height: <span class="hljs-number">100</span>,
});
</code></pre>
<h4 id="heading-server">Server</h4>
<p>There is also a server-side SDK, that you can use in your backend to create your own custom logic for access control, instead of using the service's JWT access control.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { EdgeStore } <span class="hljs-keyword">from</span> <span class="hljs-string">"@edge-store/server"</span>;

<span class="hljs-keyword">const</span> edgeStore = <span class="hljs-keyword">new</span> EdgeStore({
  accessKey: <span class="hljs-string">"your-access-key"</span>,
  secretKey: <span class="hljs-string">"your-secret-key"</span>,
});

<span class="hljs-keyword">const</span> signedUrl = <span class="hljs-keyword">await</span> edgeStore.getSignedUrl({
  name: <span class="hljs-string">"image.jpg"</span>,
});
</code></pre>
<h4 id="heading-react-component">React component</h4>
<p>I also want to build a react component to easily create customizable drag and drop image inputs in your react app. (And in the future, for other frameworks as well)</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { ImageInput } <span class="hljs-keyword">from</span> <span class="hljs-string">"@edge-store/react"</span>;

<span class="hljs-keyword">const</span> App = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> &lt;ImageInput /&gt;;
}
</code></pre>
<h2 id="heading-would-you-use-it-would-you-pay-for-it">Would you use it? Would you pay for it?</h2>
<p>When it comes to services, it's important to ensure that they are up to the mark and provide value to the user. That's why I want to know your opinion on this particular service. What do you think of it? Is it worth it to join the waiting list on the <a target="_blank" href="https://edge-store.com/">landing page</a> or comment on this post?</p>
<h2 id="heading-building-in-public">Building in public</h2>
<p>Welcome to my journey of building my first startup! I’m so excited to be starting on this journey and I hope to share the whole process with you through my <a target="_blank" href="https://youtube.com/@perfectbase">YouTube channel</a>. I want to detail the process as best as I can so that it can serve as a reference for other aspiring entrepreneurs looking to build their startups.</p>
<p>I’m sure this process is going to be a rollercoaster filled with highs and lows, and I’m sure it’s going to be one of the most challenging experiences I’ve ever faced. But I’m ready for the journey and I’m sure it’s going to be a rewarding experience.</p>
<p>Here is the first video of the journey: <a target="_blank" href="https://youtu.be/7oZw4gZjYiw">https://youtu.be/7oZw4gZjYiw</a></p>
<h2 id="heading-thanks-for-reading">Thanks for reading!</h2>
<p>👋 Here are my links!</p>
<ul>
<li><p><a target="_blank" href="https://www.youtube.com/@perfectbase">YouTube</a></p>
</li>
<li><p><a target="_blank" href="https://twitter.com/RaviCoding">Twitter</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Creating a dockerized TypeScript CLI for running batch jobs on AWS]]></title><description><![CDATA[In app development, sometimes we need to run tasks that should not go into the API. Some examples are:

Tasks that take a long time to finish.

Tasks that use a lot of CPU and memory.

Tasks that overloads you database, and need to be scheduled for w...]]></description><link>https://blog.perfectbase.dev/typescript-cli-aws-batch</link><guid isPermaLink="true">https://blog.perfectbase.dev/typescript-cli-aws-batch</guid><category><![CDATA[TypeScript]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[Docker]]></category><category><![CDATA[AWS]]></category><category><![CDATA[aws batch]]></category><dc:creator><![CDATA[Ravi]]></dc:creator><pubDate>Mon, 18 Jul 2022 12:08:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1658136254591/0fwSVMwlP.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In app development, sometimes we need to run tasks that should not go into the API. Some examples are:</p>
<ul>
<li><p>Tasks that take a long time to finish.</p>
</li>
<li><p>Tasks that use a lot of CPU and memory.</p>
</li>
<li><p>Tasks that overloads you database, and need to be scheduled for when people are not using the app. Those kinds of tasks are usually developed as batch jobs.</p>
</li>
</ul>
<p>In this tutorial we will learn how to:</p>
<ul>
<li><p>💻 Use TypeScript with Commander to create a CLI.</p>
</li>
<li><p>📦 Dockerize the cli.</p>
</li>
<li><p>🌥 Deploy and run it on AWS.</p>
</li>
</ul>
<p>It is possible that TypeScript might not be the best suited language for your use case. But the concepts from this tutorial can be used for deploying batch jobs in other languages as well. You will just need to adapt the first part.</p>
<p>※ Following this tutorial might result in some costs from used AWS Resources. (It should be just a little bit, though)</p>
<h2 id="heading-requirements">Requirements</h2>
<ul>
<li><p>Have node and npm installed.</p>
<ul>
<li>Check with <code>node --version</code> and <code>npm --version</code>.</li>
</ul>
</li>
<li><p>Have Docker installed and running.</p>
<ul>
<li>Check with <code>docker version</code> and make sure both the 'Client' and 'Server' versions are displayed.</li>
</ul>
</li>
<li><p>Have aws-cli <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">installed</a> and <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html">configured</a> for your account.</p>
<ul>
<li>Check by trying to list you s3 buckets with <code>aws s3 ls</code></li>
</ul>
</li>
</ul>
<h2 id="heading-project-structure">Project structure</h2>
<p>To start the project, run the following command from inside the projects empty folder.</p>
<pre><code class="lang-plaintext">npm init -y
</code></pre>
<p>This will generate a <code>package.json</code> file with some default values.</p>
<p>After the whole tutorial, our folder structure should look something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658136996116/mYDbJgGzk.png" alt="image.png" /></p>
<h2 id="heading-setup-typescript">Setup TypeScript</h2>
<p>First, let's install the necessary packages.</p>
<pre><code class="lang-plaintext">npm i -D typescript ts-node
npm i source-map-support
</code></pre>
<p>In the next section, we will register the <code>source-map-support</code> in our main <code>ts</code> file. This will make the error's stacktrace point to the TypeScript file instead of the compiled file.</p>
<p>Now let's make a simple configuration file for TypeScript in the root of the project.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// tsconfig.json</span>
{
  <span class="hljs-string">"compilerOptions"</span>: {
    <span class="hljs-string">"target"</span>: <span class="hljs-string">"esnext"</span>,
    <span class="hljs-string">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-string">"sourceMap"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"lib"</span>: [<span class="hljs-string">"es2022"</span>],
    <span class="hljs-string">"outDir"</span>: <span class="hljs-string">".out"</span>,
    <span class="hljs-string">"rootDir"</span>: <span class="hljs-string">"bin"</span>,
    <span class="hljs-string">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"types"</span>: [<span class="hljs-string">"node"</span>],
    <span class="hljs-string">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-string">"resolveJsonModule"</span>: <span class="hljs-literal">true</span>
  }
}
</code></pre>
<p>In the configuration above, we chose to write our TypeScript code inside <code>bin</code> and the compiled code will be outputted to <code>.out</code>. Feel free to change this to what you like.</p>
<p>※ You might want to add the following configuration to your VSCode workspace settings, so that VSCode uses the project's TypeScript version instead of the version bundled with the IDE.</p>
<pre><code class="lang-plaintext">"typescript.tsdk": "node_modules/typescript/lib"
</code></pre>
<h2 id="heading-setup-commander">Setup Commander</h2>
<p>First, we will install Commander.</p>
<pre><code class="lang-plaintext">npm i commander
</code></pre>
<p>Now we will create our first command:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// bin/commands/greeting/index.ts</span>
<span class="hljs-keyword">import</span> { Command } <span class="hljs-keyword">from</span> <span class="hljs-string">'commander'</span>;

<span class="hljs-keyword">const</span> folderName = __dirname.split(<span class="hljs-string">'/'</span>).slice(<span class="hljs-number">-1</span>)[<span class="hljs-number">0</span>];

<span class="hljs-comment">/**
 * This is an example command
 * that will take your name as an argument
 * and say hello to you in the console.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">new</span> Command()
  .command(folderName)
  .description(<span class="hljs-string">'Say hello to you!'</span>)
  .argument(<span class="hljs-string">'&lt;string&gt;'</span>, <span class="hljs-string">'Your name'</span>)
  .option(<span class="hljs-string">'-s, --suffix &lt;char&gt;'</span>, <span class="hljs-string">'Suffix greetings'</span>, <span class="hljs-string">','</span>)
  .action(<span class="hljs-function">(<span class="hljs-params">name: <span class="hljs-built_in">string</span>, options: { suffix: <span class="hljs-built_in">string</span> }</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Hello, <span class="hljs-subst">${name}</span>! <span class="hljs-subst">${options.suffix}</span>`</span>);
  });
</code></pre>
<p>Now, we will create a script that exports all commands from inside the <code>commands</code> directory. This will allow us to add new commands just by creating the command file. Without having to add a new export for every command we add.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// bin/commands/index.ts</span>
<span class="hljs-comment">/* eslint-disable @typescript-eslint/no-var-requires */</span>
<span class="hljs-keyword">import</span> { Command } <span class="hljs-keyword">from</span> <span class="hljs-string">'commander'</span>;
<span class="hljs-keyword">import</span> { readdirSync } <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;

<span class="hljs-keyword">const</span> commands: Command[] = [];

readdirSync(__dirname + <span class="hljs-string">'/'</span>).forEach(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file</span>) </span>{
  <span class="hljs-comment">// This will import all files inside the commands directory (except this one)</span>
  <span class="hljs-keyword">if</span> (!file.startsWith(<span class="hljs-string">'index.'</span>)) commands.push(<span class="hljs-built_in">require</span>(<span class="hljs-string">'./'</span> + file).default);
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> commands;
</code></pre>
<p>And finally, we will create the main file that will create our cli, and add all commands to it.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// bin/index.ts</span>
#!<span class="hljs-regexp">/usr/</span>bin/env node
<span class="hljs-keyword">import</span> <span class="hljs-string">'source-map-support/register'</span>;
<span class="hljs-keyword">import</span> { Command } <span class="hljs-keyword">from</span> <span class="hljs-string">'commander'</span>;
<span class="hljs-keyword">import</span> commands <span class="hljs-keyword">from</span> <span class="hljs-string">'./commands'</span>;

<span class="hljs-keyword">const</span> program = <span class="hljs-keyword">new</span> Command();

program.name(<span class="hljs-string">'cli'</span>).description(<span class="hljs-string">'TypeScript CLI'</span>).version(<span class="hljs-string">'0.0.0'</span>);

commands.forEach(<span class="hljs-function">(<span class="hljs-params">cmd</span>) =&gt;</span> {
  program.addCommand(cmd);
});

program.parse();
</code></pre>
<h2 id="heading-run-the-cli-with-ts-node">Run the CLI with ts-node</h2>
<p>Now we can run our command.</p>
<pre><code class="lang-plaintext">npx ts-node bin/index.ts greeting Ravi -s 'Nice name!'

&gt; Hello, Ravi! Nice name!
</code></pre>
<p>We can add this command as a script in our <code>package.json</code></p>
<pre><code class="lang-json">{
  <span class="hljs-comment">// ...</span>
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"cli"</span>: <span class="hljs-string">"ts-node bin/index.ts"</span>
  },
  <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Now we can run the same command with:</p>
<pre><code class="lang-plaintext">npm run cli -- greeting Ravi -s 'Nice name!'
</code></pre>
<h2 id="heading-dockerize">Dockerize</h2>
<p>Let's start by creating the <code>Dockerfile</code>.</p>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> node:<span class="hljs-number">16.16</span>.<span class="hljs-number">0</span>-slim

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /cli</span>

<span class="hljs-comment"># Leverage the cached layers to only reinstall packages</span>
<span class="hljs-comment"># if there are changes to `package.json` or `package-lock.json`</span>
<span class="hljs-keyword">COPY</span><span class="bash"> package.json package-lock.json ./</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npm ci</span>

<span class="hljs-comment"># Copy the rest of the files and compile it to javascript</span>
<span class="hljs-keyword">COPY</span><span class="bash"> . .</span>
<span class="hljs-keyword">RUN</span><span class="bash"> npx tsc \
    &amp;&amp; chmod +x .out/index.js</span>

<span class="hljs-keyword">ENTRYPOINT</span><span class="bash"> [ <span class="hljs-string">".out/index.js"</span> ]</span>
</code></pre>
<p>We want to ignore some files in the <code>COPY</code> command. For this we will create a <code>.dockerignore</code> file.</p>
<pre><code class="lang-plaintext"># .dockerignore
node_modules
.out

Dockerfile
.dockerignore
</code></pre>
<p>Now we can build it by running:</p>
<pre><code class="lang-bash">docker build -t typescript-cli .

<span class="hljs-comment"># If you are using a Mac device with an apple chip (M1 or M2), build with the following command:</span>
<span class="hljs-comment"># This is needed to run on Fargate</span>
docker buildx build --platform=linux/amd64 -t typescript-cli .
</code></pre>
<p>And finally we can run the job with:</p>
<pre><code class="lang-plaintext">docker run --rm -it typescript-cli greeting Ravi -s 'Nice name!'

&gt; Hello, Ravi! Nice name!
</code></pre>
<h2 id="heading-push-to-ecr">Push to ECR</h2>
<p>First, we need to create the ECR repository.</p>
<ul>
<li><p>Go to the <a target="_blank" href="https://us-east-1.console.aws.amazon.com/ecr/repositories?region=us-east-1">ECR Console</a>.</p>
</li>
<li><p>Create a new private repository named <code>typescript-cli</code>.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658137590137/Wn9Hilc6J.png" alt="aws-ecr-create-repo.png" /></p>
</li>
</ul>
<p>Now, we need to <a target="_blank" href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html">push our local image to the ECR repository</a>.</p>
<ul>
<li><h2 id="heading-login-to-ecr">Login to ECR.</h2>
</li>
</ul>
<pre><code class="lang-plaintext">aws ecr get-login-password --region &lt;region&gt; | docker login --username AWS --password-stdin &lt;aws_account_id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com
</code></pre>
<ul>
<li><p>※ Replace the &lt;***&gt; with your information.</p>
</li>
<li><h2 id="heading-tag-your-image-for-the-ecr-repository">Tag your image for the ECR repository</h2>
</li>
</ul>
<pre><code class="lang-plaintext">docker tag typescript-cli:latest &lt;aws_account_id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com/typescript-cli:latest
</code></pre>
<ul>
<li><h2 id="heading-push-your-image">Push your image</h2>
</li>
</ul>
<pre><code class="lang-plaintext">docker push &lt;aws_account_id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com/typescript-cli:latest
</code></pre>
<h2 id="heading-setup-aws-batch">Setup AWS Batch</h2>
<p>Now, we are ready to setup the job in AWS Batch</p>
<h3 id="heading-create-an-execution-role">Create an Execution Role</h3>
<p>For AWS Batch to be able to access the ECS related services, we need to create an IAM Role for it. (We're going to use it when creating the 'Job Definition')</p>
<p>First, let's go to the <a target="_blank" href="https://us-east-1.console.aws.amazon.com/iamv2/home?region=us-east-1#/roles">IAM &gt; Roles</a> console. And click in 'Create role'.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065874914/BBycKghQ0.png" alt="aws-iam-create-role-1.png" /></p>
<p>Then, search for the 'Elastic Container Service' and select 'Elastic Container Service Task'.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065541874/KjyRzTiEz.png" alt="aws-iam-create-role-2.png" /></p>
<p>Next, we need to search for 'ecs' and select 'AmazonECSTaskExecutionRolePolicy'. This will give the necessary permissions to access the ECR image we pushed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065549822/90kGpRgVM.png" alt="aws-iam-create-role-3.png" /></p>
<p>Finally, we just choose a name for the role, and click 'Create role'. Let's name it 'ecsTaskExecutionRole'.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065560737/Rt-rSMFge.png" alt="aws-iam-create-role-4.png" /></p>
<h3 id="heading-create-a-compute-environment">Create a Compute Environment</h3>
<p>Let's create a Compute Environment.</p>
<p>For that, we need to go to the <a target="_blank" href="https://us-east-1.console.aws.amazon.com/batch/home?region=us-east-1#compute-environments">Batch console &gt; Compute Environment</a>. And then create a new environment.</p>
<p>The only thing we need to set here is the [Compute environment name]. Let's set it to 'ts-batch'.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065591952/jtufxD35v.png" alt="aws-batch-compute-environment.png" /></p>
<p>The rest can be kept on the default values.</p>
<p>※ Make sure Fargate is selected on the provisioning model.<br />※ Usually we want to run the Batch job inside a <strong>Private</strong> Subnet. But for the demo, just keep the defaults and it should work.</p>
<h3 id="heading-create-a-job-queue">Create a Job Queue</h3>
<p>After we see the Compute Environment created as 'Valid' and 'Enabled'. We can create our job queue.</p>
<p>Go to the job queue tab, and create one.</p>
<p>Just set the [Job queue name] as 'ts-batch-queue' and select the compute environment we created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065635484/WfWCAL344.png" alt="aws-batch-job-queue.png" /></p>
<h3 id="heading-create-a-job-definition">Create a Job Definition</h3>
<p>Finally, we can create our job definition.</p>
<p>Here is how it looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065645055/GW3xQz99E.png" alt="aws-batch-job-definition.png" /></p>
<p>※ If your batch is running in a public subnet, you need to select the 'Assign public IP', otherwise it wont be able to access the internet and will fail trying to pull the ECR image. If it is on a private subnet with a route to a NAT Gateway on a public subnet, then you should uncheck the 'Assign public IP' since it will be able to access the internet through the NAT Gateway's IP.</p>
<h2 id="heading-run-the-job">Run the job</h2>
<p>Now we can run our job.</p>
<p>For that, in the Job Definition tab, select the job definition we've created and click on 'Submit new job'</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658065921027/zSOoIu9Ux.png" alt="aws-batch-list-job-definitions.png" /></p>
<p>Now we just need to choose a name for the job (this can be anything you like) and select the queue we've created. If we want, we can also change the default command and other values we have set in the job definition.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658066039018/dBLu3bmWR.png" alt="aws-batch-run-job.png" /></p>
<p>After that, we are able to check the running jobs on the 'Jobs' tab. (Click the refresh button if nothing shows up)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658066423921/wRLc_2K-B.png" alt="aws-batch-list-jobs.png" /></p>
<p>If we click on a job, we can see its details.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658066457856/g-IbClRJC.png" alt="aws-batch-job-status.png" /></p>
<p>And also check the logs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658066510302/9iybllqCq.png" alt="aws-batch-logs.png" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>There are many other services that allows us to run batch jobs. and this is definitely not the easiest way to do it. But if you want to leverage the AWS ecosystem, then its definitely worth it.</p>
<p>The pricing is also nice, since you basically only pay for when your jobs are running.</p>
<p>To be honest, I wish that the 'Commander' package was more typesafe. So I'll keep looking for a better package to do it. But from what I saw, 'Commander' is the most used package for building a TypeScript CLI.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<ul>
<li><p>AWS CDK (IaC)</p>
<ul>
<li>Doing everything from the console is good when you are doing it for the first time, so you can understand better how everything fits together, but for more serious projects I would consider using a tool to write all the infrastructure as code, and to be able to deploy it all with a simple command. My recommendation for that is AWS CDK. Although there are other options as well. (e.g. Terraform)</li>
</ul>
</li>
<li><p>Schedule with Event Bridge</p>
<ul>
<li>We submitted the job manually, but we can trigger it in a bunch of different ways. One way that is very common is using Event Bridge to schedule the batch job. So that it runs on a recurring schedule that you can configure.</li>
</ul>
</li>
<li><p>Choose a faster language</p>
<ul>
<li>When I can, I usually default to TypeScript. But I recognize the it might not be the best language for every project. If you need to do data manipulation, you might want to choose Python. Or if you just want to do general things, but need more performance you can choose Go or Rust. And there are many other choices out there.</li>
</ul>
</li>
</ul>
<h2 id="heading-thanks-for-reading">Thanks for reading!</h2>
<p>👋 Let's connect!</p>
<ul>
<li><p><a target="_blank" href="https://twitter.com/RaviCoding">Twitter</a></p>
</li>
<li><p><a target="_blank" href="https://www.linkedin.com/in/ravi-souza-361b935a/">LinkedIn</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Create an easily callable Modal Dialog or Snackbar Provider in React with Promise and Context]]></title><description><![CDATA[You might have noticed that with many React packages, you have to wrap your app with a Provider component.The reason for that is that those packages are using Contexts so you can easily access the component's variables and functions from anywhere in ...]]></description><link>https://blog.perfectbase.dev/react-dialog-snackbar</link><guid isPermaLink="true">https://blog.perfectbase.dev/react-dialog-snackbar</guid><category><![CDATA[React]]></category><category><![CDATA[material ui]]></category><category><![CDATA[Next.js]]></category><dc:creator><![CDATA[Ravi]]></dc:creator><pubDate>Fri, 01 Jul 2022 01:36:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1656601601015/2UXxNlLOF.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656599211678/Yk05EJ3vY.gif" alt="dialog-snackbar.gif" /></p>
<p>You might have noticed that with many React packages, you have to wrap your app with a <code>Provider</code> component.<br />The reason for that is that those packages are using <code>Contexts</code> so you can easily access the component's variables and functions from anywhere in the app.<br />And that's what we are going to be learning to do in this tutorial.</p>
<p>The following image shows how the code would look like with and without the use of a provider:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656597556610/HK1R9NWfY.png" alt="before-after.png" /></p>
<p>With the provider, you will still need to code the left part in the image. The difference is that you will do it just once, at the base of the application, and will be able to reuse it anywhere you want.</p>
<h2 id="heading-the-app-structure">The app structure</h2>
<p>I'll be using <a target="_blank" href="https://nextjs.org/">Next.js</a> and <a target="_blank" href="https://mui.com/material-ui/react-dialog/#main-content">Material UI</a> for this tutorial, but the same concepts can be replicated in a normal React application and with any other component library, or even for custom dialogs built with tailwind.</p>
<p>You can check the final code in the github repo: https://github.com/perfectbase/dialog-and-snackbar-react</p>
<p>The project was created with the following command:</p>
<pre><code>npx create<span class="hljs-operator">-</span>next<span class="hljs-operator">-</span>app@latest <span class="hljs-operator">-</span><span class="hljs-operator">-</span>typescript
</code></pre><p>And the necessary packages for Material UI were installed with this command:</p>
<pre><code><span class="hljs-built_in">npm</span> i -E @mui/material @emotion/react @emotion/styled
</code></pre><p>Here is the final folder structure:
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656599449349/mJgxyWFj3.png" alt="Screen Shot 2022-06-30 at 23.30.34.png" /></p>
<h2 id="heading-the-dialog-provider">The Dialog Provider</h2>
<p>First let's take a look on how the final provider code looks like so I can start explaining it.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// components/DialogProvider/index.tsx</span>
<span class="hljs-keyword">import</span> {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogContentText,
  DialogTitle,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@mui/material'</span>;
<span class="hljs-keyword">import</span> { createContext, useContext, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">interface</span> DialogOptions {
  title: <span class="hljs-built_in">string</span>;
  message?: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">interface</span> PromiseInfo {
  resolve: <span class="hljs-function">(<span class="hljs-params">value: <span class="hljs-built_in">boolean</span> | PromiseLike&lt;<span class="hljs-built_in">boolean</span>&gt;</span>) =&gt;</span> <span class="hljs-built_in">void</span>;
  reject: <span class="hljs-function">(<span class="hljs-params">reason?: <span class="hljs-built_in">any</span></span>) =&gt;</span> <span class="hljs-built_in">void</span>;
}

<span class="hljs-keyword">type</span> ShowDialogHandler = <span class="hljs-function">(<span class="hljs-params">options: DialogOptions</span>) =&gt;</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">boolean</span>&gt;;

<span class="hljs-comment">// Create the context so we can use it in our App</span>
<span class="hljs-keyword">const</span> DialogContext = createContext&lt;ShowDialogHandler&gt;(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Component is not wrapped with a DialogProvider.'</span>);
});

<span class="hljs-keyword">const</span> DialogProvider: React.FC&lt;{ children: React.ReactNode }&gt; = <span class="hljs-function">(<span class="hljs-params">{
  children,
}</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [open, setOpen] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [options, setOptions] = useState&lt;DialogOptions&gt;({
    title: <span class="hljs-string">''</span>,
  });
  <span class="hljs-keyword">const</span> [promiseInfo, setPromiseInfo] = useState&lt;PromiseInfo&gt;();
  <span class="hljs-keyword">const</span> showDialog: ShowDialogHandler = <span class="hljs-function">(<span class="hljs-params">options</span>) =&gt;</span> {
    <span class="hljs-comment">// When the dialog is shown, keep the promise info so we can resolve later</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      setPromiseInfo({ resolve, reject });
      setOptions(options);
      setOpen(<span class="hljs-literal">true</span>);
    });
  };
  <span class="hljs-keyword">const</span> handleConfirm = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// if the Confirm button gets clicked, resolve with `true`</span>
    setOpen(<span class="hljs-literal">false</span>);
    promiseInfo?.resolve(<span class="hljs-literal">true</span>);
    setPromiseInfo(<span class="hljs-literal">undefined</span>);
  };
  <span class="hljs-keyword">const</span> handleCancel = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">// if the dialog gets canceled, resolve with `false`</span>
    setOpen(<span class="hljs-literal">false</span>);
    promiseInfo?.resolve(<span class="hljs-literal">false</span>);
    setPromiseInfo(<span class="hljs-literal">undefined</span>);
  };
  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;Dialog open={open} onClose={handleCancel}&gt;
        &lt;DialogTitle&gt;{options.title}&lt;/DialogTitle&gt;
        &lt;DialogContent sx={{ minWidth: <span class="hljs-string">'400px'</span> }}&gt;
          {options.message &amp;&amp; (
            &lt;DialogContentText&gt;{options.message}&lt;/DialogContentText&gt;
          )}
        &lt;/DialogContent&gt;
        &lt;DialogActions&gt;
          &lt;Button onClick={handleCancel}&gt;Cancel&lt;/Button&gt;
          &lt;Button variant=<span class="hljs-string">"contained"</span> onClick={handleConfirm}&gt;
            Confirm
          &lt;/Button&gt;
        &lt;/DialogActions&gt;
      &lt;/Dialog&gt;
      &lt;DialogContext.Provider value={showDialog}&gt;
        {children}
      &lt;/DialogContext.Provider&gt;
    &lt;/&gt;
  );
};

<span class="hljs-comment">// By calling `useDialog()` in a component we will be able to use the `showDialog()` function</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useDialog = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> useContext(DialogContext);
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> DialogProvider;
</code></pre>
<p>→We want to be able to <code>await</code> until the user confirms or cancels the dialog.<br />→For that, we need to return a <code>Promise</code>.<br />→We want to resolve the <code>Promise</code> only after the user makes an action, so we keep the <code>resolve</code> function in the component's state.<br />→After the user confirms or cancels the dialog, we resolve with <code>true</code> or <code>false</code>.</p>
<h2 id="heading-wrapping-the-app-with-the-provider">Wrapping the app with the provider</h2>
<p>Now we need to wrap the app with the provider, so we can call the <code>showDialog()</code> function from anywhere on our app.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// pages/_app.tsx</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AppProps } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/app'</span>;
<span class="hljs-keyword">import</span> DialogProvider <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/DialogProvider'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">MyApp</span>(<span class="hljs-params">{ Component, pageProps }: AppProps</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;DialogProvider&gt;
      &lt;Component {...pageProps} /&gt;
    &lt;/DialogProvider&gt;
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> MyApp;
</code></pre>
<h2 id="heading-call-it-from-any-page-or-component">Call it from any page or component</h2>
<p>We can call it like this:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// ...</span>
  <span class="hljs-keyword">const</span> showDialog = useDialog();

  <span class="hljs-keyword">const</span> handleShowDialog = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> confirmed = <span class="hljs-keyword">await</span> showDialog({
      title: <span class="hljs-string">'Custom Dialog'</span>,
      message: <span class="hljs-string">'Custom message...'</span>,
    });
    <span class="hljs-keyword">if</span> (confirmed) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'confirmed'</span>);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'canceled'</span>);
    }
  };
<span class="hljs-comment">// ...</span>
</code></pre>
<p>You can see that the code awaits until you 'confirm' or 'cancel' the dialog.<br />This helps you keep your code cleaner and easy to read.</p>
<h2 id="heading-what-about-a-snackbar">What about a Snackbar?</h2>
<p>The same logic can be used to create a Snackbar Provider.</p>
<p>Here is how my code looks like:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// components/SnackbarProvider/index.tsx</span>
<span class="hljs-keyword">import</span> { Alert, Snackbar } <span class="hljs-keyword">from</span> <span class="hljs-string">'@mui/material'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AlertColor } <span class="hljs-keyword">from</span> <span class="hljs-string">'@mui/material'</span>;
<span class="hljs-keyword">import</span> { createContext, useContext, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">type</span> ShowSnackbarHandler = <span class="hljs-function">(<span class="hljs-params">message: <span class="hljs-built_in">string</span>, severity: AlertColor</span>) =&gt;</span> <span class="hljs-built_in">void</span>;

<span class="hljs-keyword">const</span> SnackbarContext = createContext&lt;ShowSnackbarHandler&gt;(<span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Component is not wrapped with a SnackbarProvider.'</span>);
});

<span class="hljs-keyword">const</span> SnackbarProvider: React.FC&lt;{ children: React.ReactNode }&gt; = <span class="hljs-function">(<span class="hljs-params">{
  children,
}</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> [open, setOpen] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [message, setMessage] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [severity, setSeverity] = useState&lt;AlertColor&gt;(<span class="hljs-string">'success'</span>);
  <span class="hljs-keyword">const</span> handleClose = <span class="hljs-function">(<span class="hljs-params">
    _event: React.SyntheticEvent | Event,
    reason?: <span class="hljs-built_in">string</span>
  </span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (reason === <span class="hljs-string">'clickaway'</span>) {
      <span class="hljs-keyword">return</span>;
    }
    setOpen(<span class="hljs-literal">false</span>);
  };
  <span class="hljs-keyword">const</span> showSnackbar: ShowSnackbarHandler = <span class="hljs-function">(<span class="hljs-params">message, severity</span>) =&gt;</span> {
    setMessage(message);
    setSeverity(severity);
    setOpen(<span class="hljs-literal">true</span>);
  };
  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;Snackbar
        open={open}
        autoHideDuration={<span class="hljs-number">4000</span>}
        onClose={handleClose}
        anchorOrigin={{ vertical: <span class="hljs-string">'top'</span>, horizontal: <span class="hljs-string">'center'</span> }}
        sx={{ minWidth: <span class="hljs-string">'50vw'</span> }}
      &gt;
        &lt;Alert onClose={handleClose} severity={severity} sx={{ width: <span class="hljs-string">'100%'</span> }}&gt;
          {message}
        &lt;/Alert&gt;
      &lt;/Snackbar&gt;
      &lt;SnackbarContext.Provider value={showSnackbar}&gt;
        {children}
      &lt;/SnackbarContext.Provider&gt;
    &lt;/&gt;
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useSnackbar = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">return</span> useContext(SnackbarContext);
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> SnackbarProvider;
</code></pre>
<p>In this case is a little simpler because we are not using Promises here.<br />You could add the Promise logic if you want your snackbar to have actions.</p>
<h2 id="heading-npm-package">npm package?</h2>
<p>I couldn't find a npm package for this. That's the main reason I decided to make this tutorial.</p>
<p>If people want, I could make a package with a customizable version of both providers. If you think this would be a good idea let me know in the comments or tell me on <a target="_blank" href="https://twitter.com/my_perfect_base">Twitter</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Learning how to use Contexts in React can open a lot of doors and it will definitely help you create the <strong>PerfectBase</strong> 😉 structure for your future apps!</p>
<h2 id="heading-example-code-repository">Example Code Repository</h2>
<p>https://github.com/perfectbase/dialog-and-snackbar-react</p>
<h3 id="heading-thanks-for-reading">Thanks for reading!</h3>
<p>👋 Let's connect!</p>
<ul>
<li><a target="_blank" href="https://twitter.com/my_perfect_base">Twitter</a></li>
<li><a target="_blank" href="https://www.linkedin.com/in/ravi-souza-361b935a/">LinkedIn</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Deploying a GraphQL API to Lambda with Serverless Framework, Apollo and TypeScript]]></title><description><![CDATA[There are a lot of ways of developing the backend of an application. Here I will be mainly talking about how to setup a Lambda based Apollo GraphQL API.In the end I will also talk about some alternative solutions, so you can choose the one that fits ...]]></description><link>https://blog.perfectbase.dev/nodejs-serverless-graphql</link><guid isPermaLink="true">https://blog.perfectbase.dev/nodejs-serverless-graphql</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[aws lambda]]></category><category><![CDATA[serverless framework]]></category><category><![CDATA[webpack]]></category><dc:creator><![CDATA[Ravi]]></dc:creator><pubDate>Tue, 28 Jun 2022 01:50:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1656381012128/3RoO-2d6I.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There are a lot of ways of developing the backend of an application. Here I will be mainly talking about how to setup a Lambda based Apollo GraphQL API.<br />In the end I will also talk about some alternative solutions, so you can choose the one that fits best for you.</p>
<p>here is the final sample code: https://github.com/perfectbase/serverless-graphql</p>
<h4 id="heading-stack">Stack</h4>
<ul>
<li><p>Serverless Framework</p>
</li>
<li><p>Webpack</p>
</li>
<li><p>TypeScript</p>
</li>
<li><p>Apollo</p>
</li>
</ul>
<h4 id="heading-some-nice-features">Some nice features</h4>
<ul>
<li><p>TypeScript</p>
<ul>
<li><p>A strongly typed language helps you prevent writing buggy code and it becomes a lot easier to refactor when you need to.</p>
</li>
<li><p>If you are using some javascript framework, chances are that you are already using TypeScript in your frontend. Using the same language in the backend can help you keep productive, while changing from backend to frontend development</p>
</li>
</ul>
</li>
<li><p>Run Locally with a fast Hot Reload (No server restart)</p>
<ul>
<li>Nowadays it's kind of unthinkable to have a development environment where you cant run the code locally, or that you need to wait a build task so that you can test your code. Nodemon has helped us with that for a long time, but a server restart also takes some seconds that can be annoying. By using <a target="_blank" href="https://www.serverless.com/plugins/serverless-offline">serverless-offline</a> you will have an almost instant hot reload, so you can modify your code and test it without breaking your thinking process.</li>
</ul>
</li>
<li><p><a target="_blank" href="https://www.apollographql.com/docs/studio">Apollo Studio</a></p>
<ul>
<li>Now with apollo 3, we have a different graphql playground. I love how Apollo made it easy to build the Queries and test the GraphQL API's.</li>
</ul>
</li>
<li><p>Cheap pricing with serverless</p>
<ul>
<li>The serverless pricing model allows you to pay just for what you use. During development, the price will probably never leave the free tier.</li>
</ul>
</li>
</ul>
<h2 id="heading-folder-structure">Folder Structure</h2>
<p>Here is how the final folder structure is going to look like:</p>
<pre><code class="lang-plaintext">sls-graphql/
├─ src/
│  ├─ functions/
│  │  ├─ graphql/
│  │  │  ├─ index.ts
│  │  │  ├─ handler.ts
│  ├─ lib/
│  │  ├─ helpers/
│  │  │  ├─ lambdaHelper.ts
├─ .babelrc
├─ .env
├─ .gitignore
├─ package-lock.json
├─ package.json
├─ serverless.ts
├─ tsconfig.json
├─ tsconfig.paths.json
├─ webpack.config.js
</code></pre>
<h2 id="heading-setup-typescript">Setup TypeScript</h2>
<p>First, lets start the node project by running the following command:</p>
<pre><code class="lang-bash">npm init -y
</code></pre>
<p>this will create a package.json file with the default configurations.</p>
<p>Now lets install some TypeScript related packages.</p>
<pre><code class="lang-bash">npm i -D typescript tsconfig-paths @types/node
</code></pre>
<p>Then we will create 2 files: <code>tsconfig.json</code> and <code>tsconfig.paths.json</code></p>
<p>Here is how mine look like.</p>
<pre><code class="lang-json"><span class="hljs-comment">// tsconfig.paths.json</span>
{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"baseUrl"</span>: <span class="hljs-string">"."</span>,
    <span class="hljs-attr">"paths"</span>: {
      <span class="hljs-attr">"@functions/*"</span>: [
        <span class="hljs-string">"src/functions/*"</span>
      ],
      <span class="hljs-attr">"@libs/*"</span>: [
        <span class="hljs-string">"src/libs/*"</span>
      ]
    }
  }
}
</code></pre>
<pre><code class="lang-json"><span class="hljs-comment">// tsconfig.json</span>
{
  <span class="hljs-attr">"extends"</span>: <span class="hljs-string">"./tsconfig.paths.json"</span>,
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"moduleResolution"</span>: <span class="hljs-string">"node"</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUnusedLocals"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noUnusedParameters"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noImplicitAny"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"noImplicitReturns"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"removeComments"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"sourceMap"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"lib"</span>: [
      <span class="hljs-string">"esnext"</span>
    ],
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"es2022"</span>,
  },
  <span class="hljs-attr">"include"</span>: [
    <span class="hljs-string">"src/**/*.ts"</span>,
    <span class="hljs-string">"serverless.ts"</span>
  ],
  <span class="hljs-attr">"ts-node"</span>: {
    <span class="hljs-attr">"require"</span>: [
      <span class="hljs-string">"tsconfig-paths/register"</span>
    ]
  }
}
</code></pre>
<p>The <code>tsconfig.paths.json</code> will allow us to import packages by using:<br /><code>import something from '@libs/helpers/...'</code><br />instead of:<br /><code>import something from '../../helpers/...'</code></p>
<p>※ If you are using VSCode, you might want to change your workspace settings, so that the editor recognizes the installed TypeScript version, instead of the default version bundled with the IDE. for that, just add the following configuration to your workspace settings json file:</p>
<pre><code class="lang-plaintext">"typescript.tsdk": "node_modules\\typescript\\lib"
</code></pre>
<h2 id="heading-setup-serverless-framework">Setup Serverless Framework</h2>
<p>For the serverless framework setup, we will need the following packages:</p>
<pre><code class="lang-plaintext">npm i -D @serverless/typescript serverless serverless-offline serverless-webpack
</code></pre>
<p>Did you know we can create the Serverless Framework configuration with a <code>.ts</code> file instead of a <code>.yml</code> file? With this you get all the auto completion and type checking functionality!</p>
<p>Here is how my <code>serverless.ts</code> looks like:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// serverless.ts</span>
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AWS } <span class="hljs-keyword">from</span> <span class="hljs-string">'@serverless/typescript'</span>;
<span class="hljs-keyword">import</span> graphql <span class="hljs-keyword">from</span> <span class="hljs-string">'@functions/graphql'</span>;

<span class="hljs-keyword">const</span> serverlessConfiguration: AWS = {
  service: <span class="hljs-string">'sls'</span>,
  frameworkVersion: <span class="hljs-string">'3'</span>,
  useDotenv: <span class="hljs-literal">true</span>,
  package: {
    individually: <span class="hljs-literal">true</span>,
  },
  custom: {
    stage: <span class="hljs-string">'${opt:stage, "local"}'</span>,
    config: {
      local: {
        NODE_ENV: <span class="hljs-string">'development'</span>,
      },
    },
    region: <span class="hljs-string">'${env:AWS_REGION}'</span>,
    webpack: {
      webpackConfig: <span class="hljs-string">'./webpack.config.js'</span>,
      includeModules: {
        <span class="hljs-comment">// You can delete this setting if you dont use aws-sdk.</span>
        <span class="hljs-comment">// If you use it, this setting will exclude it from the bundle,</span>
        <span class="hljs-comment">// since its included inside the lambda by default</span>
        forceExclude: <span class="hljs-string">'aws-sdk'</span>,
      },
    },
  },
  plugins: [<span class="hljs-string">'serverless-webpack'</span>, <span class="hljs-string">'serverless-offline'</span>],
  provider: {
    name: <span class="hljs-string">'aws'</span>,
    region: <span class="hljs-string">'${self:custom.region}'</span> <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>,
    runtime: <span class="hljs-string">'nodejs14.x'</span>,
    apiGateway: {
      minimumCompressionSize: <span class="hljs-number">1024</span>,
      shouldStartNameWithService: <span class="hljs-literal">true</span>,
    },
    environment: {
      AWS_NODEJS_CONNECTION_REUSE_ENABLED: <span class="hljs-string">'1'</span>,
      NODE_ENV:
        <span class="hljs-string">'${self:custom.config.${self:custom.stage}.NODE_ENV, "production"}'</span>,
      ENV: <span class="hljs-string">'${self:custom.stage}'</span>,
      REGION: <span class="hljs-string">'${self:custom.region}'</span>,
    },
    lambdaHashingVersion: <span class="hljs-string">'20201221'</span>,
  },
  functions: {
    graphql,
  },
};

<span class="hljs-built_in">module</span>.<span class="hljs-built_in">exports</span> = serverlessConfiguration;
</code></pre>
<p>You will also need a <code>.env</code> file for when you are ready to deploy the api to your aws account.</p>
<pre><code class="lang-plaintext">AWS_PROFILE=default
AWS_REGION=us-east-1
</code></pre>
<p>※ For deployment you will need to have your aws cli installed and configured: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html</p>
<h2 id="heading-setup-webpack">Setup Webpack</h2>
<p>Here is what we need to install for the Webpack setup:</p>
<pre><code class="lang-plaintext">npm i -D webpack webpack-node-externals @types/webpack-node-externals tsconfig-paths-webpack-plugin fork-ts-checker-webpack-plugin babel-loader @babel/core @babel/preset-env @babel/preset-typescript
</code></pre>
<p>We need a <code>.babelrc</code> file so that our TypeScript code can by transpiled.</p>
<pre><code class="lang-json"><span class="hljs-comment">// .babelrc</span>
{
  <span class="hljs-attr">"presets"</span>: [
    [
      <span class="hljs-string">"@babel/preset-env"</span>,
      {
        <span class="hljs-attr">"targets"</span>: {
          <span class="hljs-attr">"node"</span>: <span class="hljs-string">"14"</span>
        }
      }
    ],
    [
      <span class="hljs-string">"@babel/preset-typescript"</span>
    ]
  ]
}
</code></pre>
<p>Now we can add the webpack configuration:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// webpack.config.js</span>
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
<span class="hljs-keyword">const</span> slsw = <span class="hljs-built_in">require</span>(<span class="hljs-string">'serverless-webpack'</span>);
<span class="hljs-keyword">const</span> nodeExternals = <span class="hljs-built_in">require</span>(<span class="hljs-string">'webpack-node-externals'</span>);
<span class="hljs-keyword">const</span> ForkTsCheckerWebpackPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fork-ts-checker-webpack-plugin'</span>);
<span class="hljs-keyword">const</span> TsconfigPathsPlugin = <span class="hljs-built_in">require</span>(<span class="hljs-string">'tsconfig-paths-webpack-plugin'</span>);

<span class="hljs-built_in">module</span>.exports = {
  <span class="hljs-attr">context</span>: __dirname,
  <span class="hljs-attr">mode</span>: slsw.lib.webpack.isLocal ? <span class="hljs-string">'development'</span> : <span class="hljs-string">'production'</span>,
  <span class="hljs-attr">entry</span>: slsw.lib.entries,
  <span class="hljs-attr">devtool</span>: <span class="hljs-string">'source-map'</span>,
  <span class="hljs-attr">target</span>: <span class="hljs-string">'node'</span>,
  <span class="hljs-attr">externals</span>: [nodeExternals()],
  <span class="hljs-attr">resolve</span>: {
    <span class="hljs-attr">extensions</span>: [<span class="hljs-string">'.js'</span>, <span class="hljs-string">'.jsx'</span>, <span class="hljs-string">'.json'</span>, <span class="hljs-string">'.ts'</span>, <span class="hljs-string">'.tsx'</span>],
    <span class="hljs-attr">plugins</span>: [
      <span class="hljs-keyword">new</span> TsconfigPathsPlugin({
        <span class="hljs-attr">configFile</span>: <span class="hljs-string">'./tsconfig.paths.json'</span>,
      }),
    ],
  },
  <span class="hljs-attr">output</span>: {
    <span class="hljs-attr">libraryTarget</span>: <span class="hljs-string">'commonjs2'</span>,
    <span class="hljs-attr">path</span>: path.join(__dirname, <span class="hljs-string">'.webpack'</span>),
    <span class="hljs-attr">filename</span>: <span class="hljs-string">'[name].js'</span>,
  },
  <span class="hljs-attr">module</span>: {
    <span class="hljs-attr">rules</span>: [
      {
        <span class="hljs-comment">// Include ts, tsx, js, and jsx files.</span>
        <span class="hljs-attr">test</span>: <span class="hljs-regexp">/\.(ts|js)x?$/</span>,
        exclude: [<span class="hljs-regexp">/node_modules/</span>, <span class="hljs-regexp">/\.serverless/</span>, <span class="hljs-regexp">/\.webpack/</span>],
        <span class="hljs-attr">use</span>: [<span class="hljs-string">'babel-loader'</span>],
      },
    ],
  },
  <span class="hljs-attr">plugins</span>: [
    <span class="hljs-keyword">new</span> ForkTsCheckerWebpackPlugin(),
  ],
};
</code></pre>
<h2 id="heading-setup-apollo">Setup Apollo</h2>
<p>Now that we have all the structure ready, we just need to implement the api.<br />For this we will need these packages:</p>
<pre><code class="lang-bash">npm i apollo-server-lambda express source-map-support
</code></pre>
<p>Finally we can create the api functions:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/functions/graphql/handler.ts</span>
<span class="hljs-comment">// This file is the actual GraphQL API logic</span>
<span class="hljs-keyword">import</span> express <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { ApolloServer, gql } <span class="hljs-keyword">from</span> <span class="hljs-string">'apollo-server-lambda'</span>;
<span class="hljs-keyword">import</span> { IncomingMessage, OutgoingMessage } <span class="hljs-keyword">from</span> <span class="hljs-string">'http'</span>;

<span class="hljs-keyword">const</span> typeDefs = gql<span class="hljs-string">`
  type Query {
    hello: String
  }
`</span>;

<span class="hljs-keyword">const</span> resolvers = {
  Query: {
    hello: <span class="hljs-function">() =&gt;</span> <span class="hljs-string">'world'</span>,
  },
};

<span class="hljs-keyword">const</span> apolloServer = <span class="hljs-keyword">new</span> ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: <span class="hljs-literal">true</span>,
  context: ({ express }): <span class="hljs-function"><span class="hljs-params">Context</span> =&gt;</span> {
    <span class="hljs-keyword">return</span> { req: express.req, res: express.res };
  },
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Context {
  req: IncomingMessage;
  res: OutgoingMessage;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> main = apolloServer.createHandler({
  expressAppFromMiddleware(middleware) {
    <span class="hljs-keyword">const</span> app = express();
    <span class="hljs-comment">// Enable CORS for all methods</span>
    app.use(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">_req, res, next</span>) </span>{
      res.header(<span class="hljs-string">'Access-Control-Allow-Origin'</span>, <span class="hljs-string">'*'</span>);
      res.header(
        <span class="hljs-string">'Access-Control-Allow-Headers'</span>,
        <span class="hljs-string">'Origin, X-Requested-With, Content-Type, Accept'</span>
      );
      next();
    });
    app.use(middleware);
    <span class="hljs-keyword">return</span> app;
  },
});
</code></pre>
<pre><code class="lang-typescript"><span class="hljs-comment">// src/functions/graphql/index.ts</span>
<span class="hljs-comment">// This file is the lambda configuration for the api path</span>
<span class="hljs-keyword">const</span> handlerDir = <span class="hljs-string">`<span class="hljs-subst">${__dirname
  .split(process.cwd())[<span class="hljs-number">1</span>]
  .substring(<span class="hljs-number">1</span>)
  .replace(<span class="hljs-regexp">/\\/g</span>, <span class="hljs-string">'/'</span>)}</span>`</span>;

<span class="hljs-keyword">const</span> slsFunc = {
  handler: <span class="hljs-string">`<span class="hljs-subst">${handlerDir}</span>/handler.main`</span>,
  events: [
    {
      http: {
        method: <span class="hljs-string">'get'</span>,
        path: <span class="hljs-string">'graphql'</span>,
      },
    },
    {
      http: {
        method: <span class="hljs-string">'post'</span>,
        path: <span class="hljs-string">'graphql'</span>,
      },
    },
  ],
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> slsFunc;
</code></pre>
<h2 id="heading-run-and-test-the-api">Run and test the api</h2>
<p>Now that we have our api, we can run it locally with the following command:</p>
<pre><code class="lang-plaintext">npx sls offline --stage local
</code></pre>
<p>You can check your api at: http://localhost:3000/local/graphql</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1656321388461/TSBfSxWEz.png" alt="image.png" /></p>
<p>You will probably want to add this command to the <code>package.json</code> so that you can run the server by using <code>npm run dev</code>.</p>
<p>My scripts config in the package.json looks like this:</p>
<pre><code class="lang-plaintext">// package.json
{
// ...
  "scripts": {
    "dev": "sls offline --stage local",
    "deploy:dev": "sls deploy --stage dev",
    "deploy:prd": "sls deploy --stage prd"
  },
// ...
}
</code></pre>
<p>※ If your IDE shows some errors, you might need to restart your window so that it picks up all the new configurations.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>You are now ready to start adding functionality to your API!<br />I would recommend you to choose an ORM and a GraphQL library.</p>
<p>just some examples:</p>
<ul>
<li><p>ORM</p>
<ul>
<li><p>TypeORM</p>
</li>
<li><p>PRISMA</p>
</li>
</ul>
</li>
<li><p>GraphQL</p>
<ul>
<li><p>TypeGraphQL</p>
</li>
<li><p>Nexus</p>
</li>
<li><p>GraphQL Tools</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-caveats">Caveats</h2>
<ul>
<li><p>Might cost a little if you want to connect to an RDS instance.</p>
<ul>
<li>If you have an RDS instance, chances are that its is inside a private subnet. That means that if you want to connect your lambda to the RDS, you will need to put your lambda inside your VPC. If you want your lambda to have internet access from inside the VPC, you will need a NAT Gateway, that costs by the hour, and can be a little expensive for simple projects.</li>
</ul>
</li>
<li><p>It can be a little complicated to setup Subscriptions.</p>
<ul>
<li>Lambdas are short lived cloud functions. That means that they don't work well with Websockets. If you want to use Subscriptions with your lambda GraphQL api, it is possible, but you will need some setup. You could use ApiGateway for websockets, and use Redis as a PubSub, for example. There are also some serverless subscription solutions that you might want to take a look. (e.g. <a target="_blank" href="https://github.com/reconbot/graphql-lambda-subscriptions">graphql-lambda-subscriptions</a>)</li>
</ul>
</li>
</ul>
<h2 id="heading-alternatives">Alternatives</h2>
<p>We all know by now that there is no solution that works best for every project. If the solution in this article does not fit your project, here are some alternatives to check out.</p>
<h4 id="heading-amplify-with-appsync">Amplify with AppSync</h4>
<p>AWS has an out of the box serverless solution for GraphQL APIs.</p>
<ul>
<li><p>Advantages</p>
<ul>
<li><p>Works great with Cognito authentication and DynamoDB.</p>
</li>
<li><p>You can also have custom lambda resolvers.</p>
</li>
<li><p>Subscriptions works and its completely serverless.</p>
</li>
</ul>
</li>
<li><p>Disadvantages</p>
<ul>
<li><p>I didn't like the local development experience that much. Specially when using custom lambdas and running dynamodb locally on docker. It's been some time since I last tried it, maybe its better now.</p>
</li>
<li><p>If you are like me and likes to customize a lot of things, you will probably have a hard time here. (e.g. if you would like to have a Code first approach with TypeGraphQL)</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-dockerized-server-to-fargate">Dockerized server to Fargate</h4>
<p>This is probably the most customizable solution. If you choose this approach, you might want to look at some backend framework like NestJS.</p>
<ul>
<li><p>Advantages</p>
<ul>
<li><p>Its a docker environment, so you can customize it as you could do it with an old school server.</p>
</li>
<li><p>You can link AWS Secrets Manager with the container's environment variables, to increase security of secrets. (big companies require it a lot.)</p>
</li>
</ul>
</li>
<li><p>Disadvantages</p>
<ul>
<li><p>It's not serverless, so it will cost while it's running. (If you make the container small and setup auto scale, you can make it fairly cheap.)</p>
</li>
<li><p>If you want to make it scalable (running in multiple containers) you will have to think on how to setup subscriptions. (Maybe an external Redis as a PubSub?)</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-example-code-repository">Example Code Repository</h2>
<p>You might want to checkout some other configurations, like the <code>.gitignore</code> file and linter setup↓↓↓</p>
<p>github.com/perfectbase/serverless-graphql</p>
<p>Thanks for reading!<br />Follow me on Twitter: https://twitter.com/RaviCoding</p>
]]></content:encoded></item></channel></rss>