<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Chisom Uma - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Chisom Uma - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 14 May 2026 04:32:20 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/ChisomUma123/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI-Powered RAG Chatbot with Amazon Lex, Bedrock, and S3 ]]>
                </title>
                <description>
                    <![CDATA[ Chatbots are widely adopted among software companies, especially those that interact heavily with customers. It is typically used for tasks such as customer support, answering questions, and providing information on websites, apps, and messaging plat... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-ai-powered-rag-chatbot/</link>
                <guid isPermaLink="false">6930baf2becf9d70ca80fcd0</guid>
                
                    <category>
                        <![CDATA[ chatbot ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chisom Uma ]]>
                </dc:creator>
                <pubDate>Wed, 03 Dec 2025 22:34:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764801036311/e1bb9ed8-f64e-433f-916f-fd3079aac4d3.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Chatbots are widely adopted among software companies, especially those that interact heavily with customers. It is typically used for tasks such as customer support, answering questions, and providing information on websites, apps, and messaging platforms.</p>
<p>These days, as expected, some chatbots are AI-powered and can generate answers to queries through Retrieval-Augmented Generation (RAG). I have been curious about how this works, built it out myself, and now, we’ll look at how to build an AI-powered RAG chatbot.</p>
<p>For this tutorial, you’ll build a RAG chatbot that answers queries about travel policies to Mars. The chatbot retrieves its answers from our own data source (travel policy documents) stored in an S3 bucket. The document serves as our internal data source for the chatbot to reference when generating prompts.</p>
<p>Instead of scripted responses from pre-trained data, the chatbot will pull contextual answers directly from the knowledge base.</p>
<p>Let's get started :)</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-retrieval-augmented-generation-rag">What is Retrieval-Augmented Generation (RAG)?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-amazon-bedrock">What is Amazon Bedrock?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started-access-models-on-bedrock">Getting Started: Access models on Bedrock</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-upload-travel-policy-documents-to-the-s3-bucket">Step 1: Upload Travel Policy Documents to the S3 Bucket</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-a-knowledge-base-in-amazon-bedrock">Step 2: Create a Knowledge base in Amazon Bedrock</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-create-an-amazon-lex-chatbot">Step 3: Create an Amazon Lex Chatbot</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-add-a-welcome-intent-to-your-chatbot">Step 4: Add a Welcome Intent to Your Chatbot</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-build-the-chatbot">Step 5: Build the Chatbot</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-adding-amazon-qnaintent">Step 6: Adding Amazon QnAIntent</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>An AWS account, logged in as an IAM user with admin privileges</p>
</li>
<li><p>Access to Amazon Titan Embeddings G1 - text model on Amazon Bedrock</p>
</li>
<li><p>Access to Anthropic Claude 3.5 Sonnet on Amazon Bedrock.</p>
</li>
<li><p>Access to travel policy documents. You can download these from Google Drive <a target="_blank" href="https://drive.google.com/file/d/1kyewU4eCFnaYS3wQ7Fyv22G3ycthbfJb/view">here</a>.</p>
</li>
<li><p>Experience using the AWS console.</p>
</li>
<li><p>No coding required.</p>
</li>
</ul>
<h2 id="heading-what-is-retrieval-augmented-generation-rag">What is Retrieval-Augmented Generation (RAG)?</h2>
<p>Large Language Models (LLMs) like GPT-4 and Claude are basically everywhere. They get some things amazingly right and others very interestingly wrong, like hallucinations, where the model generates factually incorrect or fabricated information. This brings us to the idea of RAG.</p>
<p><em>Marina Danilevsky</em>, <em>Senior Research Scientist at IBM</em>, in a <a target="_blank" href="https://youtu.be/T-D1OfcDW1M?si=YFYcEeulZpXf9AXN">lecture</a>, referred to RAG as a “framework” for helping LLMs be more accurate and up-to-date.</p>
<p>Before going into the full scope of RAG, let’s talk briefly about the “generation” part. Generation, in the context of RAG, refers to LLMs that generate texts in response to a user query, referred to as a prompt.</p>
<p>These LLMs can sometimes give incorrect answers, due to limited context or outdated information. Especially because they only fetch information from pre-trained data. Imagine you're asked how many Grammy Awards your favorite artist has, and you give an answer you read in a magazine four years ago. You might be correct, but there are two problems with this answer: first, you didn't cite a source, and second, it's outdated.</p>
<p>This is the problem LLMs have traditionally had. The answers were outdated, and no credible sources were cited.</p>
<p>Now, imagine if you had looked up the answer first, from a reputable source on Google. Your answer would be more accurate and factual, and if there was ever a doubt from the person who asked the question, you could easily share the link to the reputable source on Google, and there would be no further doubts or questions.</p>
<p>What does this have to do with LLMs and RAG? Well, now, instead of the LLM only getting answers from its pre-trained data, risking providing outdated answers, when RAG gets involved, it retrieves answers to queries directly from a content store, which could comprise external sources, such as the internet, or internal sources, such as documents (which will be used in this tutorial). This way, its generated answers are more accurate.</p>
<p>RAG helps the LLM stay up to date by further retrieving information from other sources rather than solely from its pre-trained data.</p>
<h2 id="heading-what-is-amazon-bedrock">What is Amazon Bedrock?</h2>
<p><a target="_blank" href="https://aws.amazon.com/bedrock/?trk=68c792bf-53f8-44a0-a8eb-87bc8e0048bf&amp;sc_channel=ps&amp;ef_id=CjwKCAiAraXJBhBJEiwAjz7MZalEEwHhrurF7NUoWofbXeTPsMNnKXsegyAKvkDEfBF2f7Jd4xxwuhoCWW8QAvD_BwE:G:s&amp;s_kwcid=AL!4422!3!692062173758!e!!g!!amazon%20bedrock!21054971963!158684190945&amp;gad_campaignid=21054971963&amp;gbraid=0AAAAADjHtp__vpwZM9pm6Gjqc9UY3wYEa&amp;gclid=CjwKCAiAraXJBhBJEiwAjz7MZalEEwHhrurF7NUoWofbXeTPsMNnKXsegyAKvkDEfBF2f7Jd4xxwuhoCWW8QAvD_BwE">Amazon Bedrock</a> is AWS's managed service that gives you access to foundation models, essentially the core AI engines that power generative AI applications. The beauty of Bedrock is that it handles all the heavy lifting for you. No need to provision GPUs, set up model pipelines, or deal with infrastructure headaches.</p>
<p>It's a single platform where you can experiment with, customize, and deploy top-tier AI models from providers like <em>Anthropic</em>, <em>Stability A</em>I, and Amazon's own <em>Titan</em> models (used in this tutorial).</p>
<p>Here's a practical example: let’s say you're building a customer support chatbot. With Bedrock, you simply pick a language model that fits your needs, fine-tune it for your specific use case, and integrate it into your app, all without touching server configuration or infrastructure code.</p>
<h2 id="heading-getting-started-access-models-on-bedrock">Getting Started: Access models on Bedrock</h2>
<p>To get access to models on AWS via Bedrock:</p>
<ul>
<li><p>Log in to your AWS IAM account with root privileges.</p>
</li>
<li><p>Navigate to <strong>Amazon Bedrock &gt; Model catalog.</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764493545469/839771fb-c3aa-47d4-b1b9-60cd67ba26c5.png" alt="Image of Amazon bedrock model catalog page" class="image--center mx-auto" width="1600" height="583" loading="lazy"></p>
<ul>
<li>Locate the “Titan Embeddings G1 - Text” model and “Claude 3.5 Sonnet” models.</li>
</ul>
<p>When you click these models, you are directed to a page with more details. You don’t need to do anything on this page. We will be using these models later in this tutorial. In the following sections, we’ll walk through the steps to build the chatbot.</p>
<h2 id="heading-step-1-upload-travel-policy-documents-to-the-s3-bucket">Step 1: Upload Travel Policy Documents to the S3 Bucket</h2>
<p>To upload documents, navigate to the Amazon S3 page in your AWS console, then create a bucket. For more details on creating a bucket, refer to the <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html">AWS documentation</a>. Next, upload the downloaded document to the S3 bucket.</p>
<p>Note that the document is zipped; you will need to unzip it before uploading.</p>
<h2 id="heading-step-2-create-a-knowledge-base-in-amazon-bedrock">Step 2: Create a Knowledge base in Amazon Bedrock</h2>
<p>Now that we have created our S3 buckets and uploaded our documents, we can’t just hook up our chatbot built with Lex directly to the S3 buckets. S3 isn’t really “smart” from an AI perspective. To get the AI capabilities needed to make this work, we need Amazon Bedrock.</p>
<p>First, we need to create a knowledge base in Amazon Bedrock.</p>
<p>To get started, head back to the Bedrock page opened up earlier and navigate to <strong>Build</strong> &gt; <strong>Knowledge Bases</strong>. Click <strong>Create</strong>. From the dropdown, choose “Knowledge base with vector store.” Leave IAM permissions as “Create and use a new service role”. This is what allows Bedrock to access other services. Choose “Amazon S3” as the data source type. Click <strong>Next</strong>.</p>
<p>Next, click <strong>Browse S3</strong> and select the created bucket with the uploaded documents. Click <strong>Next</strong>. On the next page, click <strong>Select model</strong> to choose an embedding model. Select the “Titan Embeddings G1 - Text”, then select “Amazon OpenSearch Serverless” and click <strong>Apply</strong>.</p>
<p>Leave everything else the same and click <strong>Next</strong>. On the next page, click <strong>Create Knowledge Base</strong>. Note that this takes some time (a few minutes), so you need to be patient with this step. Once your knowledge base is created, you’ll be taken to a new page with a message like one in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764493716382/fd92d1d9-51ef-470f-bae9-b1cb636a260e.png" alt="image of successful Bedrock knowledge creation " class="image--center mx-auto" width="1600" height="153" loading="lazy"></p>
<p>The second message tells you that you need to sync the knowledge base with data sources. To do this, scroll down to the <em>Data source</em> section, select the data source, then click <strong>Sync</strong>. Wait a few seconds and everything syncs.</p>
<p><strong>Note:</strong> If you have more data than we have in this tutorial (just four PDFs), syncing may take longer.</p>
<p>Now, we have our Bedrock knowledge base set up. The knowledge base connects to the S3 bucket containing the travel documents.</p>
<p>It's now time to create the chatbot. For this, we’ll use <a target="_blank" href="https://aws.amazon.com/lex/">Amazon Lex</a>.</p>
<h2 id="heading-step-3-create-an-amazon-lex-chatbot">Step 3: Create an Amazon Lex Chatbot</h2>
<p>In your AWS console, navigate to Amazon Lex, then click <strong>Create bot</strong>. Select <strong>Create a blank bot</strong> under the <em>Traditional</em> creation method. For the bot name, you can call it “Mars travel bot” or any name you prefer.</p>
<p>Under the “<em>IAM permissions</em>” section, select <strong>Create a role with basic Amazon Lex permissions</strong>. Under the “<em>Children’s Online Privacy Protection Act (COPPA)</em>” section, select <strong>No,</strong> since our bot isn't subject to COPPA, and click <strong>Next</strong>.</p>
<p>On the next page, enter a short description in the <em>Description</em> text field. Select your preferred voice interaction option available for text-to-speech. This is the voice your users will hear when they use the chatbot.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764493888022/29ad9758-c0b7-42f4-8308-8255137c4649.png" alt="Image of Amazon Lex bot voices" class="image--center mx-auto" width="1414" height="984" loading="lazy"></p>
<p>The cool thing about Lex is that you can play a voice sample for each voice. This can help you make the best decision for your business. Next, click <strong>Done</strong>.</p>
<h2 id="heading-step-4-add-a-welcome-intent-to-your-chatbot">Step 4: Add a Welcome Intent to Your Chatbot</h2>
<p>After hitting the <strong>Done</strong> button, you should see a page for creating an intent next. An intent is basically an action that fulfils a user's request.</p>
<p>Let's start with creating a welcome intent. To get started, change Intent name to “WelcomeIntent”. Then scroll down to the “<em>Sample utterances</em>” section and add utterances. These are example texts that you expect a user to type or speak when they start using your chatbot. So, if the user says “Hi” the chatbot responds with a welcome message. For this tutorial, I added the following expected utterances:</p>
<ul>
<li><p>“Hi”</p>
</li>
<li><p>“Hey”</p>
</li>
<li><p>“hello”</p>
</li>
</ul>
<p>You can add as many as you want.</p>
<p>In the “<em>Initial response</em>” section, you can provide a response to the user's utterance. Under the <strong>Message group</strong> dropdown, you can type in something like “Hi! welcome! How can I help you today?” Next, click the <em>Advanced options</em> button. This reveals a dialog box. Under <em>Set values</em>, select the “Wait for users input” option. You can select other options, but for this tutorial, we are going with this. Click <strong>Update options</strong>.</p>
<p>When you navigate back to the <em>Intents</em> page, you’ll notice a “Fallbackintent” intent automatically generated for you. This intent is supposed to be invoked when a user launches your bot with an utterance that differs from the one created for the welcome intent.</p>
<h2 id="heading-step-5-build-the-chatbot">Step 5: Build the Chatbot</h2>
<p>In the previous step, we built an intent for the bot. Now it's time to build the actual chatbot that bundles up all of this configuration into something usable.</p>
<p>To get started, click <strong>Build</strong> at the top-right side of your screen.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764494047899/cac4a609-f551-4b3e-9714-1330da3e7908.png" alt="Image of building bot" class="image--center mx-auto" width="1600" height="572" loading="lazy"></p>
<p>Once the building is completed, you’ll get a message at the top of the page. Now, it’s time to test the bot. Next, click <strong>Test</strong> at the top-right side of your screen.</p>
<p>You get a pre-built chatbot for testing your implementation. Enter a text or utterance, in this case, for example, “Hi”, and you get an initial response. Remember, the utterance and initial response were set in the previous section.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764494109571/391fbcc9-b3b0-498c-85f9-43e28d12b58d.png" alt="Image of interaction with bot" class="image--center mx-auto" width="694" height="514" loading="lazy"></p>
<p>When you click on Inspect. You’ll see the current intent. In this case, the welcomeIntent.</p>
<p>At this point, we haven’t fully integrated the AI capabilities required to get answers about travel policies to Mars.</p>
<h2 id="heading-step-6-adding-amazon-qnaintent">Step 6: Adding Amazon QnAIntent</h2>
<p>The Amazon QnAIntent introduces GenAI capabilities to our bot. It is a built-in intent that uses Generative AI to fulfill Frequently Asked Questions (FAQ) requests by querying the authorized knowledge content.</p>
<p>To get started, navigate to <strong>Add intent &gt; Use built-in intent</strong> on the Intents page. Select the QnAIntent option, as shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764494196339/d28020cb-c3a2-4595-aae0-f02463d422a7.png" alt="Image of built-in intent" class="image--center mx-auto" width="1192" height="736" loading="lazy"></p>
<p>Give it a name of your choice. Click <strong>Add</strong>. You’ll be directed to the intent page. In the “<em>QnA configuration”</em> section, select <strong>Claude3.5 Sonnet</strong> as the desired model.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764494285444/9d49daed-02ed-44a6-a7e6-763611f3a214.png" alt="Image of model and knowledge base config in Lex" class="image--center mx-auto" width="1596" height="920" loading="lazy"></p>
<p>For the ID, since we had already created a knowledge base earlier, navigate back to <strong>Amazon Bedrock</strong> &gt; <strong>Knowledge Bases</strong> and copy your <em>Knowledge Base ID</em> and paste it into the “Knowledge base for Amazon Bedrock Id” field.  Click <strong>Save intent</strong>. Before testing your changes, click <strong>Build</strong> to build the bot again.</p>
<p>Now, let’s run a little test with the chatbot. I will be prompting it about items I can expense for my trip.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764494425584/65ec0a99-e438-44d3-ad62-965d82200142.png" alt="Image of AI implementation and interaction with Chabot, retrieving answers to queries from S3 via Bedrock knowledgebase" class="image--center mx-auto" width="676" height="848" loading="lazy"></p>
<p>The image above shows me having a conversation with the chatbot. I sent an utterance for the welcome intent, and it responded with a welcome message. When I asked the chatbot about what items I can expense for the trip, it pulled the information from the Bedrock knowledge base, which is connected to the S3 bucket housing the travel policy documents.</p>
<p>Try experimenting with other questions like “How much does my trip cost?” or “Can I bring my pets?”</p>
<p>Want to add a proper web UI to your bot? Follow the step-by-step instructions in this <a target="_blank" href="https://github.com/aws-samples/aws-lex-web-ui">GitHub repository</a>.</p>
<p>FYI - you should delete resources such as your knowledge base, S3 bucket, and vector store (navigate to <strong>Amazon OpenSearch Service</strong> &gt; <strong>Serverless</strong> &gt; <strong>Dashboard</strong> and delete the knowledge base vector collection) to avoid incurring any unwanted charges from AWS.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've just built an AI-powered chatbot that pulls answers from your own data sources. No more generic responses or outdated information. By combining Amazon Lex, Bedrock, S3, and RAG, you've created a system that actually understands your documentation/knowledge base and delivers accurate, contextual answers.</p>
<p>The real power here isn't just in the technology stack, it's in what you can do with it. Scale this approach to handle customer support queries, internal HR questions, product documentation, or any scenario where you need instant, accurate responses from your own knowledge base.</p>
<p>This is just the beginning. Experiment with different foundation models in Bedrock, expand your knowledge base with more documents, or refine your intents to handle more complex conversations. The infrastructure is built, now it's time to customize it for your specific use case.</p>
<p>If you found this tutorial helpful, consider sharing it with your team or fellow developers.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Full-Stack Serverless CRUD App using AWS and React ]]>
                </title>
                <description>
                    <![CDATA[ Imagine running a production application that automatically scales from zero to thousands of users without ever touching a server configuration. That's the power of serverless architecture, and it's easier to implement than you might think. If you're... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-full-stack-serverless-app/</link>
                <guid isPermaLink="false">68f7b6ca9d4df532a83f12f7</guid>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chisom Uma ]]>
                </dc:creator>
                <pubDate>Tue, 21 Oct 2025 16:37:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761064422167/c0a6b8ed-a500-43f2-820f-42fef5d73275.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Imagine running a production application that automatically scales from zero to thousands of users without ever touching a server configuration. That's the power of serverless architecture, and it's easier to implement than you might think.</p>
<p>If you're a junior cloud engineer ready to move beyond theoretical AWS concepts and build something real, this tutorial walks you through creating a complete serverless coffee shop management system.</p>
<p>You'll learn how to architect, deploy, and secure a production-ready application using AWS's most powerful serverless services.</p>
<p>Without further ado, let's get started!</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tools-well-be-using">Tools We’ll be Using</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-we-are-building">What We are Building</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-serverless">Why Serverless?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-architectural-overview">Architectural Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-build-a-serverless-full-stack-app">Build a Serverless Full-Stack App</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-create-a-dynamodb-table">Step 1: Create a DynamoDB table</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-an-iam-role-for-the-lambda-function">Step 2: Create an IAM role for the Lambda function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-create-lambda-layer-and-lambda-functions">Step 3: Create Lambda Layer And Lambda Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-create-an-api-gateway-to-expose-lambda-functions">Step 4: Create an API Gateway To Expose Lambda Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-set-up-react-application-and-upload-build-to-s3-bucket">Step 5: Set up React Application And Upload Build To S3 Bucket</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-set-up-amazon-api-gateway-authorizer">Step 6: Set up Amazon API Gateway Authorizer</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-create-cloudfront-distribution-with-behaviors-for-s3-and-api-gateway">Step 7: Create Cloudfront Distribution With Behaviors For S3 And API Gateway</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-set-up-react-application-and-upload-build-to-s3-bucket">Step 8: Set up React Application And Upload Build To S3 Bucket</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-troubleshooting-access-denied-error">Troubleshooting Access Denied Error</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-set-up-origin-access-control-oac">Step 1: Set up Origin Access Control (OAC)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-update-s3-bucket-policy">Step 2: Update S3 Bucket Policy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-set-default-root-object">Step 3: Set Default Root Object</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<ul>
<li><p>Basic knowledge of AWS.</p>
</li>
<li><p>Basic knowledge of AWS serverless services.</p>
</li>
<li><p>Knowledge of React (not required).</p>
</li>
<li><p>Basic knowledge of Postman or other API testing tools.</p>
</li>
</ul>
<h2 id="heading-tools-well-be-using">Tools We’ll be Using</h2>
<ul>
<li><p><a target="_blank" href="https://react.dev/">React.js</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/dynamodb/">DynamoDB</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/api-gateway/">API Gateway</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/pm/cognito/?trk=14a3c368-cab2-4b17-9bd6-1bbec9e89f29&amp;sc_channel=ps&amp;ef_id=Cj0KCQjw9czHBhCyARIsAFZlN8QA0K8iJKGNUsG4QX-JlA1a2EMYyCbYff2A9zo-itZdGqnDcYYJVW4aApzbEALw_wcB:G:s&amp;s_kwcid=AL!4422!3!651541907485!e!!g!!cognito!19835790380!146491699385&amp;gad_campaignid=19835790380&amp;gbraid=0AAAAADjHtp9wvSpEmU_k_hjYPjL8j0lSi&amp;gclid=Cj0KCQjw9czHBhCyARIsAFZlN8QA0K8iJKGNUsG4QX-JlA1a2EMYyCbYff2A9zo-itZdGqnDcYYJVW4aApzbEALw_wcB">Cognito</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/cloudfront/">CloudFront</a></p>
</li>
</ul>
<h2 id="heading-what-we-are-building">What We are Building</h2>
<p>We'll build a complete serverless coffee shop management system using AWS cloud services. Coffee shop owners will securely log in through AWS Cognito authentication and have full control over their inventory, adding new products, updating stock levels, viewing current inventory, and removing discontinued items. To follow along with this tutorial, you can clone the repo <a target="_blank" href="https://github.com/ChisomUma/aws-serverless-arch-project">here</a>.</p>
<p>This is what our user interface (UI) looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760784475691/8d9ba162-74dd-447d-b627-3e67b8a944ae.png" alt="image of coffee shop dashboard serverless project" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-why-serverless">Why Serverless?</h2>
<p>AWS serverless services like Lambda, Cognito, and API Gateway automatically scale to zero during quiet periods and instantly ramp up when traffic spikes. While 'serverless' might sound like there are no servers at all, this isn't actually the case. It means that AWS handles all the heavy lifting, provisioning, managing, and scaling of the infrastructure behind the scenes. You only pay for what you use.</p>
<h2 id="heading-architectural-overview">Architectural Overview</h2>
<p>Our architecture uses DynamoDB as the data store, with Lambda functions (enhanced by Lambda layers) handling all API Gateway requests. Cognito secures the API Gateway, while CloudFront CDN delivers everything globally. The React frontend connects directly to the Cognito UserPool and gets hosted on S3 with CloudFront distribution. For production deployments, you can add a custom domain using CloudFlare and AWS Certificate Manager.</p>
<h2 id="heading-build-a-serverless-full-stack-app">Build a Serverless Full-Stack App</h2>
<p>In this section, you’ll build a full-stack serverless architecture.</p>
<h3 id="heading-step-1-create-a-dynamodb-table">Step 1: Create a DynamoDB table</h3>
<p>To create a DynamoDB table, navigate to your AWS console and select the DynamoDB section. You can do this quickly by typing “DynamoDB” into the AWS search bar and clicking on DynamoDB. Next, follow the steps below to complete your table creation:</p>
<ol>
<li><p>Click <strong>Create table</strong>.</p>
</li>
<li><p>Input table name as “CoffeeShop” or anything you want to name it.</p>
</li>
<li><p>Input partition key as “coffeeId” or anything you want to name it.</p>
</li>
<li><p>Click <strong>Create table</strong>.</p>
</li>
</ol>
<p><strong>Step 1.1: Create items</strong></p>
<p>You need to create items for the table. This helps with testing connectivity to your DynamoDB table.</p>
<p>For our use case, we’ll be creating an item in the table called “coffee” and input attributes such as coffeeId, name, price, and availability. To create an item:</p>
<ol>
<li><p>Click <strong>Explore items</strong> on the left navigation pane.</p>
</li>
<li><p>Click <strong>Create items</strong>.</p>
</li>
<li><p>Click the <em>CoffeeShop</em> radio button, then click <strong>Create item</strong>.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760785698166/ee1f5e2d-feef-41de-80d8-eb2c4cad4d04.png" alt="image of dynamodb page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<ol start="4">
<li>Click <strong>Add new attribute</strong>. This allows you to add different data types such as strings and booleans. The JSON structure below shows the attributes created.</li>
</ol>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"coffeeId"</span>: <span class="hljs-string">"c123"</span>,
    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"new cold coffee"</span>,
    <span class="hljs-attr">"price"</span>: <span class="hljs-number">456</span>,
    <span class="hljs-attr">"available"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<h3 id="heading-step-2-create-an-iam-role-for-the-lambda-function">Step 2: Create an IAM role for the Lambda function</h3>
<p>Next, create a Lambda function that interacts with the DynamoDB table using an IAM role attached to the function. We’ll be setting up an IAM role named "CoffeeShopRole" that serves as a shared execution role for all Lambda functions in the coffee shop application.</p>
<p>This role includes the following permissions:</p>
<ul>
<li><p><strong>CloudWatch Logs</strong>: Full logging capabilities (create, write, and manage log streams)</p>
</li>
<li><p><strong>DynamoDB Access</strong>: Complete read, write, update, and delete operations on the "CoffeeShop" table.</p>
</li>
</ul>
<p>To do this:</p>
<ol>
<li><p>Navigate to the AWS IAM console.</p>
</li>
<li><p>Navigate to <strong>Roles</strong>.</p>
</li>
<li><p>Click <strong>Create role</strong>.</p>
</li>
<li><p>Select the Lambda service.</p>
</li>
<li><p>Search for “AWSLambdaBasicExecutionRole.”</p>
</li>
<li><p>Name your role and click <strong>Create role</strong>.</p>
</li>
</ol>
<p>This is what the role looks like:</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"VisualEditor0"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [
                <span class="hljs-string">"dynamodb:PutItem"</span>,
                <span class="hljs-string">"dynamodb:DeleteItem"</span>,
                <span class="hljs-string">"dynamodb:GetItem"</span>,
                <span class="hljs-string">"dynamodb:Scan"</span>,
                <span class="hljs-string">"dynamodb:UpdateItem"</span>
            ],
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:dynamodb::&lt;DYNAMODB_TABLE_NAME&gt;"</span>
        },
        {
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Action"</span>: [
                <span class="hljs-string">"logs:CreateLogGroup"</span>,
                <span class="hljs-string">"logs:CreateLogStream"</span>,
                <span class="hljs-string">"logs:PutLogEvents"</span>
            ],
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"*"</span>
        }
    ]
}
</code></pre>
<p>This policy allows us to create CloudWatch logs. Next, create an <strong>inline policy</strong> to allow communications to DynamoDB. Select the following actions for the table:</p>
<ul>
<li><p>Get</p>
</li>
<li><p>Put</p>
</li>
<li><p>Update</p>
</li>
<li><p>Scan</p>
</li>
<li><p>Delete</p>
</li>
</ul>
<p>Next, connect your table ARN to the policy by navigating to the created table and copying the ARN into the policy.</p>
<h3 id="heading-step-3-create-lambda-layer-and-lambda-functions">Step 3: Create Lambda Layer And Lambda Functions</h3>
<p>Now, we need to connect our Lambda function to the DynamoDB table. For this, we’ll need the DynamoDB JavaScript SDK. To get started, create two folders: <code>lambda</code> &gt; <code>get</code> in your IDE, preferably VS Code. Navigate into these folders in your terminal and run the <code>npm init</code> command to initialize your project. Update your <code>package.json</code> file with this:</p>
<pre><code class="lang-json">
{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"get"</span>,
  <span class="hljs-attr">"type"</span>: <span class="hljs-string">"module"</span>,
  <span class="hljs-attr">"version"</span>: <span class="hljs-string">"1.0.0"</span>,
  <span class="hljs-attr">"main"</span>: <span class="hljs-string">"index.js"</span>,
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"test"</span>: <span class="hljs-string">"echo \"Error: no test specified\" &amp;&amp; exit 1"</span>
  },
  <span class="hljs-attr">"author"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"license"</span>: <span class="hljs-string">"ISC"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">""</span>
}
</code></pre>
<p><strong>Note:</strong> that we’ll be using <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Glossary/ECMAScript">ECMAScript</a> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Glossary/ECMAScript">throughou</a>t the course of this tutorial.</p>
<p>Next, we have to create a reusable Node.js Lambda layer containing the <a target="_blank" href="https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_dynamodb_code_examples.html">DynamoDB JavaScript SDK</a> and shared utility functions. This layer acts like a common library that can be attached to multiple Lambda functions, eliminating the need to bundle the same dependencies repeatedly in each function's deployment package.</p>
<p>To use the SDK, create a new folder in your directory titled <code>index.mjs</code> and paste in the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-comment">// getCoffee function</span>
<span class="hljs-keyword">import</span> { DynamoDBClient, GetItemCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>; <span class="hljs-comment">// ESM import</span>
<span class="hljs-keyword">const</span> config = {
    region: <span class="hljs-string">"us-east-1"</span>,
};
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> DynamoDBClient(config);
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCoffee = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> coffeeId = <span class="hljs-string">"c123"</span>;
    <span class="hljs-keyword">const</span> input = {
        TableName: <span class="hljs-string">"CoffeShop"</span>,
        Key: {
            coffeeId: {
                S: coffeeId,
            },
        },
    };
    <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> GetItemCommand(input);
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.send(command);
    <span class="hljs-built_in">console</span>.log(response);
    <span class="hljs-keyword">return</span> response;
}
</code></pre>
<p>The code above is the <code>getCoffee</code> function that connects to the DynamoDB table called <code>CoffeShop</code>, looks up the coffee with the ID <code>c123</code>, and displays its details.</p>
<p>Change <code>region</code> to your specific region.</p>
<p>Next, install the Lambda dependencies for the SDK using the command below:</p>
<pre><code class="lang-bash">
npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
</code></pre>
<p>Then, create a zip file for all the current files using the command below:</p>
<pre><code class="lang-bash">zip -r get.zip ./*
</code></pre>
<p>This creates a zip file in your project directory. Now, navigate to the Lambda function page on your AWS console and upload this zip file.</p>
<p>Click <strong>Test</strong> to test your application. If you run into an error, edit the Runtime settings and change the handler name to <code>index.getCoffee</code>. Deploy and run the code again, you should get a successful response from DynamoDB as shown below:</p>
<p>Response:</p>
<pre><code class="lang-bash">
{
  <span class="hljs-string">"<span class="hljs-variable">$metadata</span>"</span>: {
    <span class="hljs-string">"httpStatusCode"</span>: 200,
    <span class="hljs-string">"requestId"</span>: <span class="hljs-string">"R14Q5UMTP3K9P9NAF1OGG0IB57VV4KQNSO5AEMVJF66Q9ASUAAJG"</span>,
    <span class="hljs-string">"attempts"</span>: 1,
    <span class="hljs-string">"totalRetryDelay"</span>: 0
  },
  <span class="hljs-string">"Item"</span>: {
    <span class="hljs-string">"available"</span>: {
      <span class="hljs-string">"BOOL"</span>: <span class="hljs-literal">true</span>
    },
    <span class="hljs-string">"price"</span>: {
      <span class="hljs-string">"N"</span>: <span class="hljs-string">"34"</span>
    },
    <span class="hljs-string">"name"</span>: {
      <span class="hljs-string">"S"</span>: <span class="hljs-string">"My New Coffee"</span>
    },
    <span class="hljs-string">"coffeeId"</span>: {
      <span class="hljs-string">"S"</span>: <span class="hljs-string">"c123"</span>
    }
  }
}
</code></pre>
<p>Now, let’s make the necessary changes to make our function ready for the API gateway to get the API. When someone requests a coffee using the <code>/coffee</code> endpoint, we want the app to returns a list of all coffees. But if the request is made to <code>/coffee/c123</code> or <code>/coffee/id</code>, then the app returns only details about that specific coffee.</p>
<p>To do this, head back to your <code>index.mjs</code> file and paste in the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { DynamoDBClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>;
<span class="hljs-keyword">import</span> { DynamoDBDocumentClient, GetCommand, ScanCommand } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/lib-dynamodb"</span>;
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> DynamoDBClient({});
<span class="hljs-keyword">const</span> docClient = DynamoDBDocumentClient.from(client);
<span class="hljs-keyword">const</span> tableName = process.env.tableName || <span class="hljs-string">"CoffeShop"</span>;
<span class="hljs-keyword">const</span> createResponse = <span class="hljs-function">(<span class="hljs-params">statusCode, body</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> responseBody = <span class="hljs-built_in">JSON</span>.stringify(body);
    <span class="hljs-keyword">return</span> {
        statusCode,
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        body: responseBody,
    };
};
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCoffee = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> { pathParameters } = event;
    <span class="hljs-keyword">const</span> { id } = pathParameters || {};
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">let</span> command;
        <span class="hljs-keyword">if</span> (id) {
            command = <span class="hljs-keyword">new</span> GetCommand({
                TableName: tableName,
                Key: {
                    <span class="hljs-string">"coffeeId"</span>: id,
                },
            });
        }
        <span class="hljs-keyword">else</span> {
            command = <span class="hljs-keyword">new</span> ScanCommand({
                TableName: tableName,
            });
        }
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> docClient.send(command);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">200</span>, response);
    }
    <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching data from DynamoDB:"</span>, err);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">500</span>, { error: err.message });
    }
}
</code></pre>
<p>Run the <code>zip -r get.zip ./*</code> command again and re-upload the zip file in your Lambda function page.</p>
<p>This AWS Lambda function implements a serverless API endpoint for retrieving coffee data from a DynamoDB table, using the <a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/dynamodb/command/GetItemCommand/">AWS SDK v3</a> to create a document client that can either fetch a specific coffee item by ID (when an <code>id</code> parameter is provided in the URL path) or return all items from the table (when no ID is specified, though there's a missing import for <code>ScanCommand</code>).</p>
<p>The function extracts the coffee ID from the incoming event's path parameters, constructs the appropriate DynamoDB command (<code>GetCommand</code> for single items or <code>ScanCommand</code> for all items), executes the database operation, and returns a properly formatted HTTP response with JSON headers and appropriate status codes - either a 200 success response with the coffee data or a 500 error response if something goes wrong during the database operation.</p>
<p>Repeat the steps above for the <code>create</code>, <code>update</code>, and <code>delete</code> functions. You can find these functions in your cloned <a target="_blank" href="https://github.com/ChisomUma/aws-serverless-arch-project">project repo</a>.</p>
<h3 id="heading-step-4-create-an-api-gateway-to-expose-lambda-functions">Step 4: Create an API Gateway To Expose Lambda Functions</h3>
<p>To create an API that points to the Lambda function:</p>
<ol>
<li><p>Navigate to <strong>API Gateway</strong> &gt; <strong>Routes</strong> and click <strong>Create.</strong></p>
</li>
<li><p>Create the following endpoints.</p>
</li>
</ol>
<pre><code class="lang-bash">
GET /coffee  -&gt; getCoffee lambda <span class="hljs-keyword">function</span>
GET /coffee/{id}  -&gt; getCoffee lambda <span class="hljs-keyword">function</span>
POST /coffee  -&gt; createCoffee lambda <span class="hljs-keyword">function</span>
PUT /coffee/{id}  -&gt; updateCoffee lambda <span class="hljs-keyword">function</span>
DELETE /coffee/{id}  -&gt; deleteCoffee lambda <span class="hljs-keyword">function</span>
</code></pre>
<ol start="3">
<li>Navigate to <strong>Integrations</strong> and create integrations for these endpoints. To do this, go to the <strong>Manage integrations</strong> tab, click <strong>Create,</strong> and select Lambda as the integration target.</li>
</ol>
<p>Now, in your API Gateway portal, click on <code>API: CoffeeShop...(random numbers)</code> and copy the invoke URL for testing, as shown in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760792772732/1d453e97-ce05-4be2-ae6d-d7eb55f86820.png" alt="image of postman interface during testing" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The <code>get</code> request with an <code>id</code> returns a <code>200 OK</code> response with the created items in DynamoDB. You can play around with the rest of the endpoints on Postman :)</p>
<p><strong>Adding Lambda Layer to Solve the Dependency Issue</strong></p>
<p>Before we continue with this tutorial, I’d like to address one problem with the previous steps so far. All functions use the same dependency, but for each function, we had to maintain separate <code>node_modules</code> folders and <code>packages.json</code> files. To fix this issue, we’ll be using <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html">Lamba Layer.</a> Layer contains all the dependencies, while the functions contain only your code.</p>
<p>To get started:</p>
<ol>
<li><p>Create a new folder in your IDE called <code>LambdaWithLayer</code>.</p>
</li>
<li><p>Create two additional folders under the <code>LambdaWithLayer</code> named <code>LambdaFunctionsWithLayer</code> and <code>nodejs</code>.</p>
</li>
</ol>
<p><strong>Note:</strong> You <em>must</em> use the name <code>nodejs</code> for this to work.</p>
<ol start="3">
<li><p>Navigate to the <code>nodejs</code> folder and initialize using the npm init command.</p>
</li>
<li><p>Install dependencies using the command below:</p>
</li>
</ol>
<pre><code class="lang-bash">npm i @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
</code></pre>
<ol start="5">
<li>Create a new file called <code>utils.js</code> under the <code>nodejs</code> folder and paste in the code below:</li>
</ol>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { DynamoDBClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-dynamodb"</span>;
<span class="hljs-keyword">import</span> {
    DynamoDBDocumentClient,
    ScanCommand,
    GetCommand,
    PutCommand,
    UpdateCommand,
    DeleteCommand
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/lib-dynamodb"</span>;
<span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> DynamoDBClient({});
<span class="hljs-keyword">const</span> docClient = DynamoDBDocumentClient.from(client);
<span class="hljs-keyword">const</span> createResponse = <span class="hljs-function">(<span class="hljs-params">statusCode, body</span>) =&gt;</span> {
    <span class="hljs-keyword">return</span> {
        statusCode,
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify(body),
    };
};
<span class="hljs-keyword">export</span> {
    docClient,
    createResponse,
    ScanCommand,
    GetCommand,
    PutCommand,
    UpdateCommand,
    DeleteCommand
};
</code></pre>
<p>Here, we imported all the commands for our API operations. Now, we can create Lambda Functions without installing the SDK dependencies for each one. For example, you can create a <code>get</code> folder under the <code>LambdaFunctionsWithLayer</code> folder for the <code>get</code> function, then create an <code>index.mjs</code> file under the <code>get</code> folder. Next, paste the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { docClient, GetCommand, ScanCommand, createResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'/opt/nodejs/utils.mjs'</span>; <span class="hljs-comment">// Import from Layer</span>
<span class="hljs-keyword">const</span> tableName = process.env.tableName || <span class="hljs-string">"CoffeShop"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getCoffee = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">const</span> { pathParameters } = event;
    <span class="hljs-keyword">const</span> { id } = pathParameters || {};
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">let</span> command;
        <span class="hljs-keyword">if</span> (id) {
            command = <span class="hljs-keyword">new</span> GetCommand({
                TableName: tableName,
                Key: {
                    <span class="hljs-string">"coffeeId"</span>: id,
                },
            });
        }
        <span class="hljs-keyword">else</span> {
            command = <span class="hljs-keyword">new</span> ScanCommand({
                TableName: tableName,
            });
        }
        <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> docClient.send(command);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">200</span>, response);
    }
    <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error fetching data from DynamoDB:"</span>, err);
        <span class="hljs-keyword">return</span> createResponse(<span class="hljs-number">500</span>, { error: err.message });
    }
}
</code></pre>
<p>Now we can see that, in the code, we no longer require dependencies for the <code>get</code> function. We just imported from the layer.</p>
<p>Repeat the above steps for other functions.</p>
<p><strong>Note:</strong> You can find the code for other functions in <a target="_blank" href="https://github.com/ChisomUma/aws-serverless-arch-project">the cloned repo</a>.</p>
<ol start="6">
<li>Create a zip folder for each function. You can do this by creating a file called <code>create_zip.sh</code> under the <code>LambdaFunctionsWithLayer</code> folder. Then paste the script below:</li>
</ol>
<pre><code class="lang-bash">
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for layer"</span>
zip -r layer.zip nodejs
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for GET Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/get
zip -r get.zip index.mjs
mv get.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for POST Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/post
zip -r post.zip index.mjs
mv post.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for UPDATE Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/update
zip -r update.zip index.mjs
mv update.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Creating zip for DELETE Function"</span>
<span class="hljs-built_in">cd</span> LambdaFunctionsWithLayer/delete
zip -r delete.zip index.mjs
mv delete.zip ../../
<span class="hljs-built_in">cd</span> ../..
<span class="hljs-built_in">echo</span> <span class="hljs-string">"Success!"</span>
</code></pre>
<p>Run the script using the <code>sh create_zip.sh</code> command. This creates zip files (including a <code>layer.zip</code> file) that you can upload to your AWS Lambda function Layer page.</p>
<ol start="7">
<li><p>In your AWS Lambda function page, navigate to <strong>Layers</strong> and upload the <code>layer.zip</code> file**.**</p>
</li>
<li><p>Update the functions by uploading the newly created zip files for each code.</p>
</li>
<li><p>Add the layer to the function by clicking <strong>Layers</strong> in the function view:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760793962650/8e797256-af9e-445d-8025-2fbd29dfe87f.png" alt="image of get coffee lambda layer" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, click <strong>Add a layer,</strong> then select <strong>Custom layers.</strong> Then choose <strong>“DynamoDBLayer”</strong> and version <strong>“1”.</strong></p>
<ol start="10">
<li><p>Click <strong>Add</strong>.</p>
</li>
<li><p>Repeat for all the other functions.</p>
</li>
</ol>
<h3 id="heading-step-5-set-up-react-application-and-upload-build-to-s3-bucket">Step 5: Set up React Application And Upload Build To S3 Bucket</h3>
<p>To set up our React application, navigate to the <code>frontend</code> folder of the cloned repository on your local machine and run <code>npm install</code> to install the dependencies. Then run <code>npm run dev</code> to start your development environment on your local machine. You should see the preview in your browser at: <a target="_blank" href="http://localhost:5173/"><code>http://localhost:5173/</code></a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794168737/0cace684-7f8b-47db-944a-7a642d991ca0.png" alt="image of coffe list ui" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>If you inspect the page using <a target="_blank" href="https://developer.chrome.com/docs/devtools">Chrome DevTools</a>, you’ll see that we ran into some <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS">CORS</a> error:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794416609/0cd6196e-b7cc-4f61-af5f-77995d6139ec.png" alt="image of chrome dev tool console" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, let’s fix this problem. To do that:</p>
<ol>
<li><p>Navigate your API Gateway page.</p>
</li>
<li><p>Click on <strong>CORS</strong> on the left navigation panel.</p>
</li>
<li><p>Click <strong>Configure</strong>.</p>
</li>
<li><p>Copy your <a target="_blank" href="http://localhost">localhost</a> URL and paste it into the <strong>Access-Control-Allow-Origin</strong> field.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794511537/26a1917b-16ae-48bc-a786-b36c6bb31490.png" alt="image of cors configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Ensure to remove the <code>/</code> at the end of your URL as shown in the image above.</p>
<ol start="5">
<li><p>Click <strong>Add</strong>.</p>
</li>
<li><p>Enter the <strong>Access-Control-Allow-Headers</strong> field with the text content-type and click <strong>Add</strong>.</p>
</li>
<li><p>Include <code>GET</code>, <code>POST</code>, <code>OPTIONS</code>, <code>PUT</code>, and <code>DELETE</code> in <strong>Access-Control-Allow-Methods.</strong></p>
</li>
<li><p>Click <strong>Save</strong>.</p>
</li>
</ol>
<p>Now it returns our coffee, and the CORS error has been resolved.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760794692165/7d53573d-f2e0-456d-a1da-0d06265d78a9.png" alt="image of solved cors error" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>When you add a new coffee, you should see the newly created items in your DynamoDB database.</p>
<h3 id="heading-step-6-set-up-amazon-api-gateway-authorizer">Step 6: Set up Amazon API Gateway Authorizer</h3>
<p>AWS Congnito helps you secure your Amazon API Gateway. Gateway validates the access token with Amazon Cognito to ensure it is valid and has not expired, and grants or denies access based on token validity.</p>
<p>To get started:</p>
<ol>
<li><p>Navigate to <strong>Amazon Cognito &gt; User pools</strong>.</p>
</li>
<li><p>Click <strong>Create user pool</strong>.</p>
</li>
<li><p>Select <strong>Single-page application (SPA)</strong>.</p>
</li>
<li><p>Select email as the preferred sign-in and sign-up method.</p>
</li>
<li><p>Use <code>http://localhost:5174/</code> or your own local URL as the return URL.</p>
</li>
<li><p>Click <strong>Create user directory</strong>.</p>
</li>
</ol>
<p>You’ll be presented with a page containing code that we can copy and paste into our app for integration. But before we do that, let's head back to API Gateway and integrate it with Cognito. To do that:</p>
<ol>
<li><p>Go to the Authorization section in API Gateway.</p>
</li>
<li><p>Navigate to <strong>Manage authorizers</strong>.</p>
</li>
<li><p>Click <strong>Create</strong>.</p>
</li>
<li><p>Select JWT and name it “Cognito-CoffeeShop”</p>
</li>
<li><p>Copy your issuer URL from Cognito Overview. Your issuer URL is the <em>Token signing key URL</em>. If you click on the URL, you’ll be taken to your browser, where you'll see the keys that’ll be used for verification.</p>
</li>
<li><p>For the Audience, navigate to the Cognito user pool, then to App clients, and select CoffeShopClient. Copy the Client ID.</p>
</li>
<li><p>Click <strong>Create</strong>.</p>
</li>
<li><p>Go to Routes and add authorizations to each endpoint.</p>
</li>
</ol>
<p>Now, to integrate with our front-end app:</p>
<p>Navigate into the frontend folder and run the command below:</p>
<pre><code class="lang-bash">npm install oidc-client-ts react-oidc-context --save
</code></pre>
<ol start="2">
<li><p>Go to the <strong>App clients</strong> section in Cognito user pools to find the readily available code snippets for integration.</p>
</li>
<li><p>Edit your <code>main.jsx</code> file to include the code below:</p>
</li>
</ol>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { createRoot } <span class="hljs-keyword">from</span> <span class="hljs-string">'react-dom/client'</span>
<span class="hljs-keyword">import</span> { BrowserRouter <span class="hljs-keyword">as</span> Router, Route, Routes } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'./index.css'</span>
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.jsx'</span>
<span class="hljs-keyword">import</span> ItemDetails <span class="hljs-keyword">from</span> <span class="hljs-string">"./ItemDetails"</span>;
<span class="hljs-keyword">import</span> { AuthProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-oidc-context"</span>;
<span class="hljs-keyword">const</span> cognitoAuthConfig = {
  authority: <span class="hljs-string">"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_rXq7q3KLm"</span>,
  client_id: <span class="hljs-string">"6fjfrlaup7oph5lhf1q8q6pnp4"</span>,
  redirect_uri: <span class="hljs-string">"http://localhost:5174"</span>,
  response_type: <span class="hljs-string">"code"</span>,
  scope: <span class="hljs-string">"email openid phone"</span>,
};
createRoot(<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'root'</span>)).render(
  &lt;AuthProvider {...cognitoAuthConfig}&gt;
    &lt;Router&gt;
      &lt;div&gt;
        &lt;Routes&gt;
          &lt;Route path=<span class="hljs-string">"/"</span> element={&lt;App /&gt;} /&gt;
          &lt;Route path=<span class="hljs-string">"/details/:id"</span> element={&lt;ItemDetails /&gt;} /&gt;
        &lt;/Routes&gt;
      &lt;/div&gt;
    &lt;/Router&gt;
  &lt;/AuthProvider&gt;
)
</code></pre>
<p>Here, we imported <code>AuthProvider</code> from <code>react-oidc-context</code>, then wrapped our app with <code>AuthProvider</code>.  Then, move the code in the <code>App.jsx</code> file to a newly created <code>Home.jsx</code> file, and update <code>App.jsx</code> file with the code below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./App.css"</span>;
<span class="hljs-comment">// App.js</span>
<span class="hljs-keyword">import</span> { useAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-oidc-context"</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> auth = useAuth();
  <span class="hljs-keyword">const</span> signOutRedirect = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> clientId = <span class="hljs-string">"6fjfrlaup7oph5lhf1q8q6pnp4"</span>;
    <span class="hljs-keyword">const</span> logoutUri = <span class="hljs-string">"http://localhost:5174/"</span>;
    <span class="hljs-keyword">const</span> cognitoDomain = <span class="hljs-string">"https://us-east-1rxq7q3klm.auth.us-east-1.amazoncognito.com"</span>;
    <span class="hljs-built_in">window</span>.location.href = <span class="hljs-string">`<span class="hljs-subst">${cognitoDomain}</span>/logout?client_id=<span class="hljs-subst">${clientId}</span>&amp;logout_uri=<span class="hljs-subst">${<span class="hljs-built_in">encodeURIComponent</span>(logoutUri)}</span>`</span>;
  };
  <span class="hljs-keyword">if</span> (auth.isLoading) {
    <span class="hljs-keyword">return</span> &lt;div&gt;Loading...&lt;/div&gt;;
  }
  <span class="hljs-keyword">if</span> (auth.error) {
    <span class="hljs-keyword">return</span> &lt;div&gt;Encountering error... {auth.error.message}&lt;/div&gt;;
  }
  <span class="hljs-keyword">if</span> (auth.isAuthenticated) {
    <span class="hljs-keyword">return</span> (
      &lt;div&gt;
        &lt;button onClick={<span class="hljs-function">() =&gt;</span> auth.removeUser()}&gt;Sign out&lt;/button&gt;
        &lt;Home /&gt;
      &lt;/div&gt;
    );
  }
  <span class="hljs-keyword">return</span> (
    &lt;div&gt;
      &lt;button onClick={<span class="hljs-function">() =&gt;</span> auth.signinRedirect()}&gt;Sign <span class="hljs-keyword">in</span>&lt;/button&gt;
      &lt;button onClick={<span class="hljs-function">() =&gt;</span> signOutRedirect()}&gt;Sign out&lt;/button&gt;
    &lt;/div&gt;
  );
}
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<p>Now, when you run the application again, you should see this login page on your browser:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760795002733/2be7ce35-ecff-41bb-adff-ed13c7a33a32.png" alt="Sign in and Sign out buttons" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>When you click on Sign in, you’ll get directed to the Sign in page. Click Sign up. You should see the page below to create your account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760795104086/41c44c85-881d-482c-ae1f-d84f1ea76fb5.png" alt="Sign in page with a form" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>During sign-up, a verification code is sent to your sign-up email. Once you’re logged in, you can then access your coffee dashboard.</p>
<h3 id="heading-step-7-create-cloudfront-distribution-with-behaviors-for-s3-and-api-gateway"><strong>Step 7: Create Cloudfront Distribution With Behaviors For S3 And API Gateway</strong></h3>
<p>To create a distribution.</p>
<ol>
<li><p>Navigate to <strong>CloudFront</strong>.</p>
</li>
<li><p>Click <strong>Create distribution</strong>.</p>
</li>
<li><p>In the Origin page, select the S3 bucket and browse through your created S3 buckets.</p>
</li>
<li><p>Select your coffee shop bucket.</p>
</li>
<li><p>Set origin path to <code>/dist</code>.</p>
</li>
<li><p>Select <em>Origin access control</em> under <strong>Origin access</strong>.</p>
</li>
<li><p>Update your React code and AWS Cognito with the distribution domain name provided in the CloudFront log-in pages tab.</p>
</li>
</ol>
<h3 id="heading-step-8-set-up-react-application-and-upload-build-to-s3-bucket"><strong>Step 8: Set up React Application And Upload Build To S3 Bucket</strong></h3>
<p>In this step, we’ll be building our React application and uploading the static files to an Amazon S3 bucket, which is then served from a CloudFront distribution.</p>
<p>To get started:</p>
<ol>
<li><p>Create an S3 bucket and give it the name “mycoffeeShop123new”. This name should be globally unique across all AWS accounts.</p>
</li>
<li><p>In the frontend folder, run the <code>npm run build</code> command. This creates a <code>dist</code> folder in your directory.</p>
</li>
<li><p>Head back to the S3 bucket and drag-and-drop the <code>dist</code> folder into S3 to upload it.</p>
</li>
<li><p>Click <strong>Upload</strong>.</p>
</li>
</ol>
<p>Now, copy your CloudFront distribution URL and try to access your site in a private browser, for example, Chrome incognito. You should see your site live in the browser.</p>
<h2 id="heading-troubleshooting-access-denied-error"><strong>Troubleshooting Access Denied Error</strong></h2>
<p>You may encounter an access denied error in the browser:</p>
<pre><code class="lang-xml">
<span class="hljs-tag">&lt;<span class="hljs-name">Error</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Code</span>&gt;</span>AccessDenied<span class="hljs-tag">&lt;/<span class="hljs-name">Code</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">Message</span>&gt;</span>Access Denied<span class="hljs-tag">&lt;/<span class="hljs-name">Message</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">Error</span>&gt;</span>
</code></pre>
<p>It may be because of a likely S3 + CloudFront configuration error. Here are the steps to resolve this issue:</p>
<h3 id="heading-step-1-set-up-origin-access-control-oac">Step 1: Set up Origin Access Control (OAC)</h3>
<ol>
<li><p>Go to <strong>CloudFront &gt; Your Distribution &gt; Origins tab.</strong></p>
</li>
<li><p>Select your S3 origin and click <strong>Edit.</strong></p>
</li>
<li><p>Under <strong>Origin access</strong>, select <strong>Origin access control settings (recommended)</strong></p>
</li>
<li><p>Click <strong>Create new OAC</strong> (or select an existing one).</p>
</li>
<li><p>Click <strong>Save changes.</strong></p>
</li>
</ol>
<h3 id="heading-step-2-update-s3-bucket-policy">Step 2: Update S3 Bucket Policy</h3>
<p>After saving, CloudFront will show you a <strong>"Copy Policy"</strong> button. Click it, then:</p>
<ol>
<li><p>Go to your S3 bucket &gt; <strong>Permissions</strong> tab.</p>
</li>
<li><p>Scroll to <strong>Bucket policy</strong> and click <strong>Edit.</strong></p>
</li>
<li><p>Paste the copied policy (it should look like this):</p>
</li>
</ol>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"Version"</span>: <span class="hljs-string">"2012-10-17"</span>,
    <span class="hljs-attr">"Statement"</span>: [
        {
            <span class="hljs-attr">"Sid"</span>: <span class="hljs-string">"AllowCloudFrontServicePrincipal"</span>,
            <span class="hljs-attr">"Effect"</span>: <span class="hljs-string">"Allow"</span>,
            <span class="hljs-attr">"Principal"</span>: {
                <span class="hljs-attr">"Service"</span>: <span class="hljs-string">"cloudfront.amazonaws.com"</span>
            },
            <span class="hljs-attr">"Action"</span>: <span class="hljs-string">"s3:GetObject"</span>,
            <span class="hljs-attr">"Resource"</span>: <span class="hljs-string">"arn:aws:s3:::YOUR-BUCKET-NAME/*"</span>,
            <span class="hljs-attr">"Condition"</span>: {
                <span class="hljs-attr">"StringEquals"</span>: {
                    <span class="hljs-attr">"AWS:SourceArn"</span>: <span class="hljs-string">"arn:aws:cloudfront::YOUR-ACCOUNT-ID:distribution/YOUR-DISTRIBUTION-ID"</span>
                }
            }
        }
    ]
}
</code></pre>
<ol start="4">
<li>Click <strong>Save changes.</strong></li>
</ol>
<h3 id="heading-step-3-set-default-root-object"><strong>Step 3: Set Default Root Object</strong></h3>
<ol>
<li><p>Go back to <strong>CloudFront &gt; Your Distribution &gt; General</strong> tab.</p>
</li>
<li><p>Click <strong>Edit.</strong></p>
</li>
<li><p>Set <strong>Default root object</strong> to <code>index.html</code>.</p>
</li>
<li><p>Save changes.</p>
</li>
</ol>
<p>Now, try accessing the site again. It should work.</p>
<p>This brings us to the end of this tutorial. I hope you were able to learn a thing or two about building serverless systems :)</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations! You've just built a production-ready serverless application from the ground up. You've successfully architected a complete CRUD system that automatically scales, stays secure with Cognito authentication, and costs you only what you actually use.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Upload Large Objects to S3 with AWS CLI Multipart Upload ]]>
                </title>
                <description>
                    <![CDATA[ Uploading large files to S3 using traditional single-request methods can be quite challenging. If you’re transferring a 5GB database backup, and a network interruption happens, it forces you to restart the entire upload process. This wastes bandwidth... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-upload-large-objects-to-s3-with-aws-cli-multipart-upload/</link>
                <guid isPermaLink="false">688b03991b7d57429769e76b</guid>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chisom Uma ]]>
                </dc:creator>
                <pubDate>Thu, 31 Jul 2025 05:48:09 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753940874032/9dae199c-ff29-44c1-aff9-4dad02fdc26d.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Uploading large files to S3 using traditional single-request methods can be quite challenging. If you’re transferring a 5GB database backup, and a network interruption happens, it forces you to restart the entire upload process. This wastes bandwidth and time. And this approach becomes increasingly unreliable as file sizes grow.</p>
<p>With a single PUT operation, you can actually upload an object of up to 5 GB. But, when it comes to uploading larger objects (above 5GB), using Amazon S3’s <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html">Multipart Upload</a> feature is a better approach.</p>
<p>Multipart upload makes it easier for you to upload larger files and objects by segmenting them into smaller, independent chunks that upload separately and reassemble on S3.</p>
<p>In this guide, you’ll learn how to implement multipart uploads using AWS CLI.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-multipart-uploads-work">How Multipart Uploads Work</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started">Getting started</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-step-1-download-the-aws-cli">Step 1: Download the AWS CLI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-configure-aws-iam-credentials">Step 2: Configure AWS IAM credentials</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-split-object">Step 1: Split Object</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-create-an-amazon-s3-bucket">Step 2: Create an Amazon S3 bucket</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-initiate-multipart-upload">Step 3: Initiate Multipart Upload</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-upload-split-files-to-s3-bucket">Step 4: Upload split files to S3 Bucket</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-create-a-json-file-to-compile-etag-values">Step 5: Create a JSON File to Compile ETag Values</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-complete-mulitipart-upload-to-s3">Step 6: Complete Mulitipart Upload to S3</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow this guide, you should have:</p>
<ul>
<li><p>An AWS account.</p>
</li>
<li><p>Knowledge of AWS and the S3 service.</p>
</li>
<li><p>AWS CLI is installed on your local machine.</p>
</li>
</ul>
<h2 id="heading-how-multipart-uploads-work">How Multipart Uploads Work</h2>
<p>In a multipart upload, large file transfers are segmented into smaller chunks that get uploaded separately to Amazon S3. After all segments complete their upload process, S3 reassembles them into the complete object.</p>
<p>For example, a 160GB file broken into 1GB segments generates 160 individual upload operations to S3. Each segment receives a distinct identifier while preserving sequence information to guarantee proper file reconstruction.</p>
<p>The system supports configurable retry logic for failed segments and allows upload suspension/resumption functionality. Here’s a diagram that shows what the multipart upload process looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753729484012/63a68bcc-8c2d-4110-a007-bd67a3b4b2e4.png" alt="AWS multipart upload process" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Before you get started with this guide, make sure that you have the AWS CLI installed on your machine. If you don’t already have that installed, follow the steps below.</p>
<h3 id="heading-step-1-download-the-aws-cli"><strong>Step 1: Download the AWS CLI</strong></h3>
<p>To download the CLI, visit the <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">CLI download documentation</a>. Then, download the CLI based on your operating system (Windows, Linux, macOS). Once the CLI is installed, the next step is to configure your AWS IAM credentials in your terminal.</p>
<h3 id="heading-step-2-configure-aws-iam-credentials"><strong>Step 2: Configure AWS IAM credentials</strong></h3>
<p>To configure your AWS credentials, navigate to your terminal and run the command below:</p>
<pre><code class="lang-bash">aws configure
</code></pre>
<p>This command prompts you to paste in certain credentials, such as AWS Access Key ID and secret ID. To obtain these credentials, create a new IAM user in your AWS account. To do this, follow the steps below. (You can skip these steps if you already have an IAM user and security credentials.)</p>
<ol>
<li><p><a target="_blank" href="https://eu-north-1.signin.aws.amazon.com/oauth?client_id=arn%3Aaws%3Asignin%3A%3A%3Aconsole%2Fcanvas&amp;code_challenge=gHNgZ4aX7afSDOY05TLWJNFrXDRnWRy-Mn3_TqW2p9o&amp;code_challenge_method=SHA-256&amp;response_type=code&amp;redirect_uri=https%3A%2F%2Fconsole.aws.amazon.com%2Fconsole%2Fhome%3FhashArgs%3D%2523%26isauthcode%3Dtrue%26nc2%3Dh_si%26src%3Dheader-signin%26state%3DhashArgsFromTB_eu-north-1_bcd5b75451c14744">Sign in</a> to your AWS dashboard.</p>
</li>
<li><p>Click on the search bar above your dashboard and search “IAM”.</p>
</li>
<li><p>Click on IAM.</p>
</li>
<li><p>In the left navigation pane, navigate to <strong>Access management</strong> &gt; <strong>Users</strong>.</p>
</li>
<li><p>Click <strong>Create user</strong>.</p>
</li>
<li><p>During IAM user creation, attach a policy directly by selecting <strong>Attach policies directly</strong> in step 2: Set permissions.</p>
</li>
<li><p>Give the user admin access by searching “admin” in the permission policies search bar and selecting AdministratorAccess.</p>
</li>
<li><p>On the next page, click <strong>Create user</strong>.</p>
</li>
<li><p>Click on the created user in the <strong>Users</strong> section and navigate to <strong>Security credentials</strong>.</p>
</li>
<li><p>Scroll down and click <strong>Create access key</strong>.</p>
</li>
<li><p>Select the Command Line Interface (CLI) use case.</p>
</li>
<li><p>On the next page, click <strong>Create access key</strong>.</p>
</li>
</ol>
<p>You will now see your access keys. <strong>Please keep these safe</strong> and do not expose them publicly or share them with anyone.</p>
<p>You can now copy these access keys into your terminal after running the <code>aws configure</code> command.</p>
<p>You will be prompted to include the following details:</p>
<ul>
<li><p><code>AWS Access Key ID</code>: gotten from the created IAM user credentials. See steps above.</p>
</li>
<li><p><code>AWS Secret Access Key</code>: gotten from the created IAM user credentials. See steps above.</p>
</li>
<li><p><code>Default region name</code>: default AWS region name, for example, <code>us-east-1</code>.</p>
</li>
<li><p><code>Default output format</code>: None.</p>
</li>
</ul>
<p>Now we’re done with the CLI configuration.</p>
<p>To confirm that you’ve successfully installed the CLI, run the command below:</p>
<pre><code class="lang-bash">aws --version
</code></pre>
<p>You should see the CLI version in your terminal as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753730059205/5164dd17-5f66-40ba-b044-f1bca46a22a0.png" alt="Image of AWS CLI version" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, you are ready for the following main steps for multipart uploads :)</p>
<h2 id="heading-step-1-split-object">Step 1: Split Object</h2>
<p>The first step is to split the object you intend to upload. For this guide, we’ll be splitting a <strong>188MB</strong> video file into smaller chunks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753730190692/bd22598d-0267-4507-864d-f3f7397faf19.png" alt="Image of object size" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Note that this process also works for much larger files.</p>
<p>Next, locate the object you intend to upload in your system. You can use the <code>cd</code> command to locate the object in its stored folder using your terminal.</p>
<p>Then run the split command below:</p>
<pre><code class="lang-bash">split -b &lt;SIZE&gt;mb &lt;filename&gt;
</code></pre>
<p>Replace <code>&lt;SIZE&gt;</code> with your desired chunk size in megabytes (for example, 150, 100, 200).</p>
<p>For this use case, we’ll be splitting our 188mb video file into bytes. Here’s the command:</p>
<pre><code class="lang-bash">
split -b 31457280 videoplayback.mp4
</code></pre>
<p>Next, run the <code>ls -lh</code> command on your terminal. You should get the output below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753730408994/33ef3552-9a92-46e9-9c2a-686e73d23c65.png" alt="Image of split object" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Here, you can see that the 188MB file has been split into multiple parts (30MB and 7.9MB). When you go to the folder where the object is saved in your system files, you will see additional files with names that look like this:</p>
<ul>
<li><p><code>xaa</code></p>
</li>
<li><p><code>xab</code></p>
</li>
<li><p><code>xac</code></p>
</li>
</ul>
<p>and so on. These files represent the different parts of your object. For example, <code>xaa</code> is the first part of your file, which will be uploaded first to S3. More on this later in the guide.</p>
<h2 id="heading-step-2-create-an-amazon-s3-bucket">Step 2: Create an Amazon S3 Bucket</h2>
<p>If you don’t already have an S3 bucket created, follow the steps in the AWS <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/GetStartedWithS3.html">Get Started with Amazon S3</a> documentation to create one.</p>
<h2 id="heading-step-3-initiate-multipart-upload">Step 3: Initiate Multipart Upload</h2>
<p>The next step is to initiate a multipart upload. To do this, execute the command below:</p>
<pre><code class="lang-bash">
aws s3api create-multipart-upload --bucket DOC-EXAMPLE-BUCKET --key large_test_file
</code></pre>
<p>In this command:</p>
<ul>
<li><p><code>DOC-EXAMPLE-BUCKET</code> is your S3 bucket name.</p>
</li>
<li><p><code>large_test_file</code> is the name of the file, for example, videoplayback.mp4.</p>
</li>
</ul>
<p>You’ll get a JSON response in your terminal, providing you with the <code>UploadId</code><strong>.</strong> The response looks like this:</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"ServerSideEncryption"</span>: <span class="hljs-string">"AES345"</span>,
    <span class="hljs-attr">"Bucket"</span>: <span class="hljs-string">"s3-multipart-uploads"</span>,
    <span class="hljs-attr">"Key"</span>: <span class="hljs-string">"videoplayback.mp4"</span>,
    <span class="hljs-attr">"UploadId"</span>: <span class="hljs-string">"************************************"</span>
}
</code></pre>
<p>Keep the <code>UploadId</code> somewhere safe in your local machine, as you will need it for later steps.</p>
<h2 id="heading-step-4-upload-split-files-to-s3-bucket">Step 4: Upload Split Files to S3 Bucket</h2>
<p>Remember those extra files saved as xaa, xab, and so on? Well, now it’s time to upload them to your S3 bucket. To do that, execute the command below:</p>
<pre><code class="lang-bash">aws s3api upload-part --bucket DOC-EXAMPLE-BUCKET --key large_test_file --part-number 1 --body large_test_file.001 --upload-id exampleTUVGeKAk3Ob7qMynRKqe3ROcavPRwg92eA6JPD4ybIGRxJx9R0VbgkrnOVphZFK59KCYJAO1PXlrBSW7vcH7ANHZwTTf0ovqe6XPYHwsSp7eTRnXB1qjx40Tk
</code></pre>
<ul>
<li><p><code>DOC-EXAMPLE-BUCKET</code> is your S3 bucket name.</p>
</li>
<li><p><code>large_test_file</code> is the name of the file, for example, videoplayback.mp4</p>
</li>
<li><p><code>large_test_file.001</code> is the name of the file part, for example, xaa.</p>
</li>
<li><p><code>upload-id</code> replaces the example ID with your saved <code>UploadId</code>.</p>
</li>
</ul>
<p>The command returns a response that contains an <strong>ETag</strong> value for the part of the file that you uploaded.</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"ServerSideEncryption"</span>: <span class="hljs-string">"aws:kms"</span>,
    <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"7f9b8c3e2a1d5f4e8c9b2a6d4e8f1c3a\""</span>,
    <span class="hljs-attr">"ChecksumCRC64NVME"</span>: <span class="hljs-string">"mK9xQpD2WnE="</span>
}
</code></pre>
<p>Copy the <strong>ETag</strong> value and save it somewhere on your local machine, as you’ll need it later as a reference.</p>
<p>Continue uploading the remaining file parts by repeating the command above, incrementing both the part number and file name for each subsequent upload. For example: <code>xaa</code> becomes <code>xab</code>, and <code>--part-number 1</code> becomes <code>--part-number 2</code>, and so forth.</p>
<p>Note that upload speed depends on how large the object is and how good your internet speed is.</p>
<p>To confirm that all the file parts have been uploaded successfully, run the command below:</p>
<pre><code class="lang-bash">aws s3api list-parts --bucket s3-multipart-uploads --key videoplayback.mp4 --upload-id p0NU3agC3C2tOi4oBmT8lHLebUYqYXmWhEYYt8gc8jXlCStEZYe1_kSx1GjON2ExY_0T.4N4E6pjzPlNcji7VDT6UomtNYUhFkyzpQ7IFKrtA5Dov8YdC20c7UE20Qf0
</code></pre>
<p>Replace the example upload ID with your actual upload ID.</p>
<p>You should get a JSON response like this:</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"Parts"</span>: [
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">1</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:22:18+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"f7b9c8e4d3a2f6e8c9b5a4d7e6f8c2b1\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">26214400</span>
        },
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">2</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:25:42+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"a8e5d2c7f9b4e6a3c8d5f2e9b7c4a6d3\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">26214400</span>
        },
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">3</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:28:15+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"c4f8e2b6d9a3c7e5f8b2d6a9c3e7f4b8\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">26214400</span>
        },
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">4</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:31:03+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"e9c3f7a5d8b4e6c9f2a7d4b8c6e3f9a2\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">26214400</span>
        },
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">5</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:33:47+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"b6d4a8c7f5e9b3d6a2c8f4e7b9c5d8a6\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">26214400</span>
        },
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">6</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:36:29+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"d7e3c9f6a4b8d2e5c7f9a3b6d4e8c2f5\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">26214400</span>
        },
        {
            <span class="hljs-attr">"PartNumber"</span>: <span class="hljs-number">7</span>,
            <span class="hljs-attr">"LastModified"</span>: <span class="hljs-string">"2025-07-27T14:38:52+00:00"</span>,
            <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"f2a6d8c4e7b3f6a9c2d5e8b4c7f3a6d9\""</span>,
            <span class="hljs-attr">"Size"</span>: <span class="hljs-number">15728640</span>
        }
    ]
}
</code></pre>
<p>This is how you verify that all parts have been uploaded.</p>
<h2 id="heading-step-5-create-a-json-file-to-compile-etag-values">Step 5: Create a JSON File to Compile ETag Values</h2>
<p>The document we are about to create helps AWS understand which parts the ETags represent. Gather the <strong>ETag</strong> values from each uploaded file part and organize them into a JSON structure.</p>
<p>Sample JSON format:</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"Parts"</span>: [{
        <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"example8be9a0268ebfb8b115d4c1fd3"</span>,
        <span class="hljs-attr">"PartNumber"</span>:<span class="hljs-number">1</span>
    },

    ....

    {
        <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"example246e31ab807da6f62802c1ae8"</span>,
        <span class="hljs-attr">"PartNumber"</span>:<span class="hljs-number">4</span>
    }]
}
</code></pre>
<p>Save the created JSON file in the same folder as your object and name it <code>multipart.json</code>. You can use any IDE of your choice to create and save this document.</p>
<h2 id="heading-step-6-complete-mulitipart-upload-to-s3">Step 6: Complete Mulitipart Upload to S3</h2>
<p>To complete the multipart upload, run the command below:</p>
<pre><code class="lang-bash">aws s3api complete-multipart-upload --multipart-upload file://fileparts.json --bucket DOC-EXAMPLE-BUCKET --key large_test_file --upload-id exampleTUVGeKAk3Ob7qMynRKqe3ROcavPRwg92eA6JPD4ybIGRxJx9R0VbgkrnOVphZFK59KCYJAO1PXlrBSW7vcH7ANHZwTTf0ovqe6XPYHwsSp7eTRnXB1qjx40Tk
</code></pre>
<p>Replace <code>fileparts.json</code> with <code>multipart.json</code>.</p>
<p>You should get an output like this:</p>
<pre><code class="lang-json">
{
    <span class="hljs-attr">"ServerSideEncryption"</span>: <span class="hljs-string">"AES256"</span>,
    <span class="hljs-attr">"Location"</span>: <span class="hljs-string">"https://s3-multipart-uploads.s3.eu-west-1.amazonaws.com/videoplayback.mp4"</span>,
    <span class="hljs-attr">"Bucket"</span>: <span class="hljs-string">"s3-multipart-uploads"</span>,
    <span class="hljs-attr">"Key"</span>: <span class="hljs-string">"videoplayback.mp4"</span>,
    <span class="hljs-attr">"ETag"</span>: <span class="hljs-string">"\"78298db673a369adf33dd8054bb6bab7-7\""</span>,
    <span class="hljs-attr">"ChecksumCRC64NVME"</span>: <span class="hljs-string">"d1UPkm73mAE="</span>,
    <span class="hljs-attr">"ChecksumType"</span>: <span class="hljs-string">"FULL_OBJECT"</span>
}
</code></pre>
<p>Now, when you go to your S3 bucket and hit refresh, you should see the uploaded object.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1753731196794/c7e121d4-f3a7-4d23-95c5-26b1b5e3b699.png" alt="Image of object successfully uploaded to AWS using multipart upload" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Here, you can see the complete file, file name, type, and size.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Multipart uploads transform large file transfers to Amazon S3 from fragile, all-or-nothing operations into robust, resumable processes. By segmenting files into manageable chunks, you gain retry capabilities, better performance, and the ability to handle objects exceeding S3's 5GB single-upload limit.</p>
<p>This approach is essential for production environments dealing with database backups, video files, or any large assets. With the AWS CLI techniques covered in this guide, you're now equipped to handle S3 transfers confidently, regardless of file size or network conditions.</p>
<p>Check out this <a target="_blank" href="https://repost.aws/knowledge-center/s3-multipart-upload-cli">documentation from the AWS knowledge center</a> to learn more about multi-part uploads using AWS CLI.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Documentation Site using React and Docusaraus ]]>
                </title>
                <description>
                    <![CDATA[ Having a properly designed and comprehensive documentation site is important for any project. But creating good documentation can be challenging, and problems like poor user onboarding experience and increased support tickets can become daily hassles... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-documentation-site-using-react-and-docusaraus/</link>
                <guid isPermaLink="false">6706b5ae1446e5644b75b252</guid>
                
                    <category>
                        <![CDATA[ docusaurus ]]>
                    </category>
                
                    <category>
                        <![CDATA[ documentation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Technical writing  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Chisom Uma ]]>
                </dc:creator>
                <pubDate>Wed, 09 Oct 2024 16:56:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728407506914/49f6f7cd-af92-405e-ac89-d71bd74e3f18.avif" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Having a properly designed and comprehensive documentation site is important for any project. But creating good documentation can be challenging, and problems like poor user onboarding experience and increased support tickets can become daily hassles for a team.</p>
<p>This is why documentation tools like Docusaurus are great for helping you create visually stunning documentation sites in abo 5 minutes.</p>
<p>In this tutorial, I'll show you how to build a documentation site using React and Docusaurus.</p>
<p>If you are new to Docusaurus, you are probably wondering, “why React?, why not any other framework like Next.js?”, Don’t worry – I’ll also answer this question in this guide.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-docusaurus">What is Docusaurus?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started-and-installation">Getting Started and Installation</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-project-structure">Project structure</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-start-your-docusaurus-website">How to Start Your Docusaurus Website</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-documentation-overview">How to Create Documentation (Overview)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-mdx-and-react-components">MDX and React Components</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-tabs">Tabs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-alerts-or-admonitions">Alerts or Admonitions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-code-blocks">Code blocks</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-docusaurus-blog">Docusaurus Blog</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-custom-pages">Custom Pages</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-styling-in-docusaurus">Styling in Docusaurus</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-deployment">Deployment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this guide, you should have:</p>
<ul>
<li><p><strong>Node.js</strong> version 18.0 or above installed</p>
</li>
<li><p>Basic knowledge of React and Markdown</p>
</li>
<li><p>IDE (preferably VSCode)</p>
</li>
</ul>
<h2 id="heading-what-is-docusaurus">What is Docusaurus?</h2>
<p><a target="_blank" href="https://docusaurus.io/">Docusaurus</a> was released by the Meta Open Source team in 2017 to help devs build, deploy, and maintain documentation websites easily and quickly. The project’s other goal was to improve the speed of both developers and end users using the <a target="_blank" href="https://web.dev/articles/apply-instant-loading-with-prpl">PRPL</a> pattern.</p>
<p>Some of its cool features include search and localization, powered by <a target="_blank" href="https://www.algolia.com/">Algolia</a> (search) and <a target="_blank" href="https://crowdin.com/">Crowdin</a> (language support and internationalization).</p>
<p>Now, let’s talk about why we’re using React for this tutorial. Well, Docusaurus is built on top of React, which makes it easy to customize the website. Also, Docusaurus supports Markdown and MDX (which lets you use React JSX in your markdown content).</p>
<p>As a technical writer myself, I love that this tool supports Markdown, which I'm quite familiar with. It allows me to focus on creating helpful documentation without worrying about converting the text to other text formats.</p>
<h2 id="heading-getting-started-and-installation">Getting Started and Installation</h2>
<p>Getting started with Docusaraus is pretty straightforward. The first thing you need to do is head over to your terminal and run the command below:</p>
<pre><code class="lang-plaintext">npx create-docusaurus@latest my-website classic
</code></pre>
<p><strong>Note:</strong> The Docusaurus team recommends the <code>classic</code> template because it is easier to get started with fast. It also contains <code>@docusaurus/preset-classic</code> – which includes standard documentation, a blog, custom pages, and a CSS framework (with dark mode support).</p>
<h3 id="heading-project-structure">Project structure</h3>
<p>After installation, this is what your newly created Docusaurus project structure should look like:</p>
<pre><code class="lang-plaintext">📦my-website
┣ 📂blog
┃ ┣ 📂2021-08-26-welcome
┃ ┃ ┣ 📜docusaurus-plushie-banner.jpeg
┃ ┃ ┗ 📜index.md
┃ ┣ 📜2019-05-28-first-blog-post.md
┃ ┣ 📜2019-05-29-long-blog-post.md
┃ ┣ 📜2021-08-01-mdx-blog-post.mdx
┃ ┣ 📜authors.yml
┃ ┗ 📜tags.yml
┣ 📂docs
┃ ┣ 📂tutorial-basics
┃ ┃ ┣ 📜congratulations.md
┃ ┃ ┣ 📜create-a-blog-post.md
┃ ┃ ┣ 📜create-a-document.md
┃ ┃ ┣ 📜create-a-page.md
┃ ┃ ┣ 📜deploy-your-site.md
┃ ┃ ┣ 📜markdown-features.mdx
┃ ┃ ┗ 📜_category_.json
┃ ┣ 📂tutorial-extras
┃ ┃ ┣ 📂img
┃ ┃ ┃ ┣ 📜docsVersionDropdown.png
┃ ┃ ┃ ┗ 📜localeDropdown.png
┃ ┃ ┣ 📜manage-docs-versions.md
┃ ┃ ┣ 📜translate-your-site.md
┃ ┃ ┗ 📜_category_.json
┃ ┗ 📜intro.md
┣ 📂src
┃ ┣ 📂components
┃ ┃ ┗ 📂HomepageFeatures
┃ ┃ ┃ ┣ 📜index.js
┃ ┃ ┃ ┗ 📜styles.module.css
┃ ┣ 📂css
┃ ┃ ┗ 📜custom.css
┃ ┗ 📂pages
┃ ┃ ┣ 📜index.js
┃ ┃ ┣ 📜index.module.css
┃ ┃ ┗ 📜markdown-page.md
┣ 📂static
┃ ┣ 📂img
┃ ┃ ┣ 📜docusaurus-social-card.jpg
┃ ┃ ┣ 📜docusaurus.png
┃ ┃ ┣ 📜favicon.ico
┃ ┃ ┣ 📜logo.svg
┃ ┃ ┣ 📜undraw_docusaurus_mountain.svg
┃ ┃ ┣ 📜undraw_docusaurus_react.svg
┃ ┃ ┗ 📜undraw_docusaurus_tree.svg
┃ ┗ 📜.nojekyll
┣ 📜.gitignore
┣ 📜babel.config.js
┣ 📜docusaurus.config.js
┣ 📜package.json
┣ 📜README.md
┗ 📜sidebars.js
</code></pre>
<p>Now, let’s explore some of the main directories:</p>
<ul>
<li><p><code>blog/</code>: This is where you store your blog posts</p>
</li>
<li><p><code>docs/</code>: As the name implies, this is where your documentation projects are kept</p>
</li>
<li><p><code>src/</code>: This directory allows you to customize your website further using React code.</p>
</li>
<li><p><code>static</code>: Here, you store static files like images, logos, favicons, and so on.</p>
</li>
</ul>
<p>A very important file is the <code>docusaurus.config.js</code> file, which acts as the main configuration file for your website.</p>
<h2 id="heading-how-to-start-your-docusaurus-website">How to Start Your Docusaurus Website</h2>
<p>To run your website locally, first open a new terminal on your IDE and run the following command below to start the development server:</p>
<pre><code class="lang-plaintext">cd my-website

npm i

npx docusaurus start
</code></pre>
<p>After running the above command, the browser compiles the React and Markdown files and starts a local development server at <a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a>. Docusaurus enables hot reload, so you can see changes made to your React, Markdown, and MDX files automatically – without reloading your entire app.</p>
<p>Here is what the site looks like on your browser:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728389930307/b0bd7810-dea2-458b-85a1-e10b9a7b3028.png" alt="Bootstrapped Docusaurus site" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In the image above, you are first welcomed to an intuitive and easily navigable website. At the top left corner of the website, you will see the “<strong>Tutorial</strong>” and “<strong>Blog</strong>” sections.</p>
<ul>
<li><p><strong>Tutorial:</strong> This is where users can see the live version of your documentation.</p>
</li>
<li><p><strong>Blog:</strong> This is where users can see the live version of your blog.</p>
</li>
</ul>
<p>The link to the Docusaurus Open Source GitHub repo and the icon for toggling your website between light and dark modes are at the top-right corner of the site.</p>
<p>Alternatively, you can use <a target="_blank" href="https://docusaurus.io/docs/playground">docusaurus.new</a> to test Docusaurus quickly in a playground without having to go through the installation process. Here, you have an option to choose between <a target="_blank" href="https://codesandbox.io/">CodeSandbox</a> and <a target="_blank" href="https://stackblitz.com/">StackBlitz</a>.</p>
<h2 id="heading-how-to-create-documentation-overview">How to Create Documentation (Overview)</h2>
<p>Let’s take a closer look at our <code>docs</code> folder. If we head back to our local site and click on “<strong>Tutorial</strong>,” we will see some pre-built doc pages, as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728390116953/ec281a01-5b0c-413a-83b9-36d0352f3e03.png" alt="Documentation overview on the site" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>These documentation pages are reflected in the <code>docs</code> folder located in your IDE. When we open the <code>category.json</code> page, we can adjust the name or position of each page. This means you don’t have to name the folders the same as the page name, since the name of the file will be the URL of the page.</p>
<p>To create our new documentation, let’s use the following steps:</p>
<ol>
<li><p>Delete all the files in the docs folder. Your browser and terminal will typically display an error after this.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728390294195/5a59bdc4-7a79-4b17-9e85-630867c6c3ec.png" alt="Error from deleted files" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p> This is because we need at least one page in the docs folder.</p>
</li>
<li><p>Create a new file inside the docs folder, which can be named anything you prefer, but in our case, I named it <a target="_blank" href="http://single-page.md">single-page.md</a>. You should see this change immediately reflected when you go to your local website. This is what you will see in the documentation pages section:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728390512797/90b86f29-8b03-414b-acff-18842cd4c462.png" alt="Single page " class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
</li>
</ol>
<p>Inside this newly created file, you can create your documentation seamlessly. The image above shows the little Markdown content I created saying “Single Page” in an H1 and “This is a single page” in plain text.</p>
<p>Let’s say you want to create a more organized content structure, but you don’t know how to get started. Here are a few simple steps on how to do that:</p>
<ol>
<li><p>Create a new folder inside the <code>docs</code> folder, named “Getting Started”</p>
</li>
<li><p>Create new Markdown files inside the “Getting started” folder, and name them whatever you like. For the sake of this tutorial, let’s name it <a target="_blank" href="http://API-docs.md"><code>API-docs.md</code></a> and <a target="_blank" href="http://Session-replay.md"><code>Session-replay.md</code></a>.</p>
</li>
<li><p>Write your documentation in Markdown</p>
</li>
</ol>
<p>This is how the file structure should look like on your IDE:</p>
<pre><code class="lang-plaintext">📦docs
┣ 📂Getting started
┃ ┣ 📜Open-replay.md
┃ ┗ 📜Session-replay.md
┗ 📜single-page.md
</code></pre>
<p>Here is a simple GIF to demonstrate how this works on the local documentation website.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728390784981/9eed8cbf-c6dc-4508-9d75-401d87673cf7.gif" alt="GIF showing local documentation site" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, let’s try to create a separate page in the <code>doc</code> folder. To do this, let’s create a  <code>category.json</code> page in the <code>Getting started</code> folder. Inside the  <code>category.json</code>  file, we will include the following JSON text:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"label"</span>: <span class="hljs-string">"Custom Title"</span>,
  <span class="hljs-attr">"position"</span>: <span class="hljs-number">2</span>,
  <span class="hljs-attr">"link"</span>: {
    <span class="hljs-attr">"type"</span>: <span class="hljs-string">"generated-index"</span>,
    <span class="hljs-attr">"description"</span>: <span class="hljs-string">"This is a custom description"</span>
  }
}
</code></pre>
<ul>
<li><p>The <code>link</code> object creates a separate page for the folder.</p>
</li>
<li><p>The <code>type</code> property is set to generated-index, which means it will generate the pages with all the sub-pages.</p>
</li>
<li><p>The <code>description</code> property is a description of the page that will show up beneath the title.</p>
</li>
</ul>
<p>When you check out your local hosted documentation site, you will see that the label has changed, and a separate page has been created for the folder.</p>
<p>In this section, I will show you an example of how headings and tables of content work in Docusaurus.</p>
<p>The first thing I did was create a <a target="_blank" href="http://markdown.md"><code>markdown.md</code></a> file. Then I pasted a couple of headings in Markdown text format right inside the file, like this:</p>
<pre><code class="lang-markdown">---
title: Basic Markdown
<span class="hljs-section">sidebar<span class="hljs-emphasis">_position: 1
---

# Heading 1

## Heading 2

### Heading 3

#### Heading 4

##### Heading 5

###### Heading 6</span></span>
</code></pre>
<p>When we head back to our website, we can see that only headings level 2 and 3 are automatically added, just as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728391234366/1ba2a824-3d31-4fd2-bd3e-8fcb9547e073.png" alt="Image showing headers" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>We can edit to ensure that all the headings show up. To do this, first create a <a target="_blank" href="http://table-of-contents.md"><code>table-of-contents.md</code></a>, then paste in the following Markdown:</p>
<pre><code class="lang-markdown">---
title: Table of Contents
sidebar<span class="hljs-emphasis">_position: 2
toc_</span>min<span class="hljs-emphasis">_heading_</span>level: 2
<span class="hljs-section">toc<span class="hljs-emphasis">_max_</span>heading<span class="hljs-emphasis">_level: 6
---

import TOCInline from '@theme/TOCInline';

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TOCInline</span> <span class="hljs-attr">toc</span>=<span class="hljs-string">{toc}</span> <span class="hljs-attr">minHeadingLevel</span>=<span class="hljs-string">{2}</span> <span class="hljs-attr">maxHeadingLevel</span>=<span class="hljs-string">{6}</span> /&gt;</span></span>

## Heading 2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.

### Heading 3

Some content here.

#### Heading 4

Some content here.

##### Heading 5

Some content here.

###### Heading 6

Some content here.

## Heading 2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.

### Heading 3

Some content here.

#### Heading 4

Some content here.

##### Heading 5

Some content here.

###### Heading 6

Some content here.

## Heading 2

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor.

### Heading 3

Some content here.

#### Heading 4

Some content here.

##### Heading 5

Some content here.

###### Heading 6

Some content here.</span></span>
</code></pre>
<p>I added a TOC property and set the minimum and maximum heading levels with <code>toc_min_heading_level: 2</code> and <code>toc_max_heading_level: 6</code>. We also added an inline table of contents by first importing <code>TOCInline</code> from <code>@theme/TOCInline</code>. Then, we created a TOCInline component (which can be put anywhere you want your TOC to show up).</p>
<p>Now, all the headings show up in the table of contents part of the website:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728398728652/37595594-3dbc-42bc-853c-f5b5ba9714c4.png" alt="Image showing table of content and headers" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Beautiful right?</p>
<h2 id="heading-mdx-and-react-components">MDX and React Components</h2>
<p>Now, let’s talk about one of the most exciting features of Docusaurus: MDX and React components.</p>
<p>You might ask – how can Docusaurus use <code>TOC</code> or <code>import</code> in the Markdown file? Well, that’s because Docusaurus uses MDX, which is basically Markdown with JSX.</p>
<p>To demonstrate this, let’s create a new Markdown file inside our <code>Getting started</code> folder titled  <a target="_blank" href="http://MDX.md"><code>MDX.md</code></a> , then we create a separate file inside the <code>src</code> &gt; <code>components</code> folder and name the file <code>Tag.js</code> . Then we paste in the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<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">Tag</span>(<span class="hljs-params">{ children, color }</span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">span</span>
      <span class="hljs-attr">style</span>=<span class="hljs-string">{{</span>
        <span class="hljs-attr">backgroundColor:</span> <span class="hljs-attr">color</span>,
        <span class="hljs-attr">borderRadius:</span> '<span class="hljs-attr">4px</span>',
        <span class="hljs-attr">color:</span> '#<span class="hljs-attr">fff</span>',
        <span class="hljs-attr">padding:</span> '<span class="hljs-attr">0.2rem</span> <span class="hljs-attr">0.5rem</span>',
        <span class="hljs-attr">fontWeight:</span> '<span class="hljs-attr">bold</span>',
      }}
    &gt;</span>
      {children}
    <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span></span>
  );
}
</code></pre>
<p>Here, we first imported the core React library, and then we defined a functional component called Tag, which takes in two props as input: <code>children</code> and <code>color</code>. In our return statement, we included our CSS styles for the <code>span</code> element.</p>
<p>Inside the <a target="_blank" href="http://MDX.md">MDX.md</a> file, paste in the below code:</p>
<pre><code class="lang-markdown">---
title: MDX
<span class="hljs-section">sidebar<span class="hljs-emphasis">_position: 3
---

import Tag from '@site/src/components/Tag';

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Tag</span> <span class="hljs-attr">color</span>=<span class="hljs-string">"#FF5733"</span>&gt;</span></span>Important<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">Tag</span>&gt;</span></span> information: This is an <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Tag</span> <span class="hljs-attr">color</span>=<span class="hljs-string">"#3399FF"</span>&gt;</span></span>Exciting<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">Tag</span>&gt;</span></span> example of custom components!

I can write <span class="hljs-strong">**Markdown**</span> alongside my _</span>JSX<span class="hljs-emphasis">_!</span></span>
</code></pre>
<p>Here, we import <code>Tag</code> from our components folder. This is what it looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728398996580/a826b80c-1862-46f4-b111-dc6366dd13db.png" alt="Image showing how MDX works" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><strong>Note:</strong> Because we use MDX, Docusaurus comes with pre-built components like Tabs, alerts, code blocks, and more. Let’s check them out in the following subsections.</p>
<h3 id="heading-tabs">Tabs</h3>
<p>In this subsection, we’ll talk about tabs as a pre-built component in Docusaurus. Let’s dive right in!</p>
<p>For a start, let’s create a new Markdown file called <a target="_blank" href="http://tabs.md"><code>tabs.md</code></a> and paste in the following code:</p>
<pre><code class="lang-markdown">---
title: Tabs in Markdown
<span class="hljs-section">sidebar<span class="hljs-emphasis">_position: 4
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Tabs</span>&gt;</span></span>
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TabItem</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"book"</span> <span class="hljs-attr">label</span>=<span class="hljs-string">"Book"</span> <span class="hljs-attr">default</span>&gt;</span></span>
    Dive into the world of knowledge with a captivating book 📚
  <span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">TabItem</span>&gt;</span></span>
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TabItem</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"painting"</span> <span class="hljs-attr">label</span>=<span class="hljs-string">"Painting"</span>&gt;</span></span>
    Admire the strokes of artistry on a beautiful painting 🖼️
  <span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">TabItem</span>&gt;</span></span>
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">TabItem</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"music"</span> <span class="hljs-attr">label</span>=<span class="hljs-string">"Music"</span>&gt;</span></span>
    Let the soothing melodies of music transport you 🎶
  <span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">TabItem</span>&gt;</span></span>
<span class="xml"><span class="hljs-tag">&lt;/<span class="hljs-name">Tabs</span>&gt;</span></span>

I'm a text that doesn't belong to any tab. So I'm always visible.</span></span>
</code></pre>
<p>We imported <code>Tabs</code> from <code>@theme/Tabs</code> and <code>TabItem</code> from <code>@theme/TabItem</code>. Then, we created a Tabs component, which is the container, and the <code>TabItem</code> component is the tab itself. The <code>value</code> property is the value of the tab, while the <code>label</code> property is the label of the tab. The default property defines which tab is open by default – in this case, the “Book” tab.</p>
<p>This is how it looks:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728399214390/edece46f-3357-4b96-8672-a462a8a8916b.png" alt="Image showing how tabs work" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Each tab is clickable and transitions smoothly.</p>
<h3 id="heading-alerts-or-admonitions">Alerts or Admonitions</h3>
<p>Docusaurus comes with pre-built alerts or admonitions. To create an alert, you simply wrap the text with three colons and follow it with the type of alert you want the reader to have in mind.</p>
<p>Let’s create a new Markdown file called <a target="_blank" href="http://alerts.md">alerts.md</a> and paste in the following Markdown:</p>
<pre><code class="lang-markdown">---
title: Alerts or Admonitions
<span class="hljs-section">sidebar<span class="hljs-emphasis">_position: 5
---

:::note

Here's some <span class="hljs-strong">**information**</span> with _</span>Markdown<span class="hljs-emphasis">_ styling.

:::

:::tip

Here's a <span class="hljs-strong">**helpful tip**</span> with _</span>formatted text<span class="hljs-emphasis">_.

:::

:::info

Here's some <span class="hljs-strong">**useful info**</span> presented in a clear way.

:::

:::caution

Please take <span class="hljs-strong">**extra caution**</span> with this important note.

:::

:::danger

This is a <span class="hljs-strong">**dangerous situation**</span> you need to be aware of.

:::

:::note This is a _</span>custom title<span class="hljs-emphasis">_

And you can add images as well.

![<span class="hljs-string">alt text</span>](<span class="hljs-link">https://picsum.photos/600/400</span>)

:::</span></span>
</code></pre>
<p>The various types of alerts, as shown in the Markdown above, are:</p>
<ul>
<li><p><code>note</code></p>
</li>
<li><p><code>tip</code></p>
</li>
<li><p><code>info</code></p>
</li>
<li><p><code>caution</code></p>
</li>
<li><p><code>danger</code></p>
</li>
</ul>
<p>Here’s what it looks like on the website:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728402667575/cc10af2d-e417-4426-985b-4aad81a082db.png" alt="Image showing how alerts and admonitions work" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Alerts and admonitions are pretty common in documentation sites. So, if you have ever wondered how it’s been done, well, this is it! It’s quite straightforward.</p>
<h3 id="heading-code-blocks">Code blocks</h3>
<p>As we already know by now, Docusaurus supports MDX, which allows us to include code blocks in our files. Code blocks are text blocks wrapped around by strings of three backticks. You can add the name of the language after the last of the three backticks.</p>
<p>Let’s create a <a target="_blank" href="http://codeblocks.md"><code>codeblocks.md</code></a> file and paste the following JSX code:</p>
<pre><code class="lang-javascript">---
title: Codeblocks
<span class="hljs-attr">sidebar_position</span>: <span class="hljs-number">6</span>
---

<span class="hljs-string">``</span><span class="hljs-string">`jsx title="Codeblock"
function Greeting(props) {
  return &lt;p&gt;Welcome, {props.userName}!&lt;/p&gt;;
}

export default Greeting;
`</span><span class="hljs-string">``</span>

<span class="hljs-string">``</span><span class="hljs-string">`jsx title="Highlight Lines"
function HighlightText(highlight) {
  if (highlight) {
    // highlight-next-line
    return 'This text is highlighted!';
  }
  return 'Nothing highlighted';
}

function HighlightMoreText(highlight) {
  // highlight-start
  if (highlight) {
    return 'This range is highlighted!';
  }
  // highlight-end
  return 'Nothing highlighted';
}
`</span><span class="hljs-string">``</span>

<span class="hljs-string">``</span><span class="hljs-string">`jsx title="Line Numbers" showLineNumbers
import React from 'react';

function UserProfile(props) {
  const { username, email, isAdmin } = props;

  return (
    &lt;div&gt;
      &lt;h1&gt;User Profile&lt;/h1&gt;
      &lt;p&gt;Username: {username}&lt;/p&gt;
      &lt;p&gt;Email: {email}&lt;/p&gt;
      {isAdmin &amp;&amp; &lt;p&gt;Admin User&lt;/p&gt;}
    &lt;/div&gt;
  );
}

export default UserProfile;
`</span><span class="hljs-string">``</span>

<span class="hljs-string">``</span><span class="hljs-string">`jsx title="Line Numbers with Highlight" {4,9-11} showLineNumbers
import React from 'react';

function UserProfile(props) {
  const { username, email, isAdmin } = props;

  return (
    &lt;div&gt;
      &lt;h1&gt;User Profile&lt;/h1&gt;
      &lt;p&gt;Username: {username}&lt;/p&gt;
      &lt;p&gt;Email: {email}&lt;/p&gt;
      {isAdmin &amp;&amp; &lt;p&gt;Admin User&lt;/p&gt;}
    &lt;/div&gt;
  );
}

export default UserProfile;</span>
</code></pre>
<p>This is what the code blocks look like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728402852316/4873fccd-d4b7-45d7-8d4f-49fea5a3da49.png" alt="Image showing how codeblocks work" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can easily copy the code by hovering your cursor over the code blocks and clicking on the copy icon on the top-right side of the code block.</p>
<p><strong>Note:</strong> By default, Docusaurus uses <code>Prism</code> for syntax highlighting.</p>
<p>If you also want to highlight some lines of code, you can do that by adding a comment like this:</p>
<pre><code class="lang-javascript">    <span class="hljs-comment">// highlight-next-line</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">'This text is highlighted!'</span>;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-string">'Nothing highlighted'</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HighlightMoreText</span>(<span class="hljs-params">highlight</span>) </span>{
  <span class="hljs-comment">// highlight-start</span>
  <span class="hljs-keyword">if</span> (highlight) {
    <span class="hljs-keyword">return</span> <span class="hljs-string">'This range is highlighted!'</span>;
  }
  <span class="hljs-comment">// highlight-end</span>
</code></pre>
<ul>
<li><p><code>highlight-next-line</code>: allows you to highlight a single line of code</p>
</li>
<li><p><code>highlight-start and highlight-end</code>: allows you to highlight a range of lines</p>
</li>
</ul>
<h2 id="heading-docusaurus-blog">Docusaurus Blog</h2>
<p>The Docusaurus blog also comes by default with the <code>classic</code> template. If you don’t have a blog, you can add the following lines to your <code>docusaurus.config.js</code> file:</p>
<pre><code class="lang-javascript">{
  <span class="hljs-attr">label</span>: <span class="hljs-string">'Blog'</span>,
  <span class="hljs-attr">to</span>: <span class="hljs-string">'/blog'</span>,
}
</code></pre>
<p>Usually, this line should already be in your config file after installing Docusaurus for the first time.</p>
<p>The Docusaurus blog is very simple to understand. Navigate to the blog folder in the project explorer, and you’ll see all the blog posts, which are MDX files. The blog post date should be included on the file name as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728403567052/c60d665d-f29b-433e-bd10-b86542d0063e.png" alt="Image showing blog folder" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>When you open one of the blog posts, you see something like this:</p>
<pre><code class="lang-markdown">---
slug: long-blog-post
title: Long Blog Post
authors: yangshun
<span class="hljs-section">tags: [hello, docusaurus]
---</span>

This is the summary of a very long blog post,

Use a <span class="hljs-code">`&lt;!--`</span> <span class="hljs-code">`truncate`</span> <span class="hljs-code">`--&gt;`</span> comment to limit blog post size in the list view.

<span class="xml"><span class="hljs-comment">&lt;!-- truncate --&gt;</span></span>

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet
</code></pre>
<ul>
<li><p><code>slug</code>: You can add a slug to the URL of the blog post</p>
</li>
<li><p><code>title</code>: Title of the blog post</p>
</li>
<li><p><code>authors</code>: The authors of the blog post</p>
</li>
<li><p><code>tags</code>: Tags related to the blog post</p>
</li>
</ul>
<p>In the blog post, we can also use all the Markdown features plus JSX as we have seen before.</p>
<h2 id="heading-custom-pages">Custom Pages</h2>
<p>Technically, Docusaurus isn’t just a fancy documentation site generator with a blog. It’s a standard static site generator – which means you can create any page you want.</p>
<p>To see how creating a custom page in Docusaurus works, let’s create an about.js file in the <code>src</code> <strong>&gt;</strong> <code>pages</code> folder and include the following code:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> React <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> Layout <span class="hljs-keyword">from</span> <span class="hljs-string">'@theme/Layout'</span>;
<span class="hljs-keyword">import</span> Head <span class="hljs-keyword">from</span> <span class="hljs-string">'@docusaurus/Head'</span>;

<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">About</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Layout</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">Head</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"description"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"This is the about page"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">Head</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"red-text"</span>&gt;</span>About<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>This is the about page.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">Layout</span>&gt;</span></span>
  );
}
</code></pre>
<p>When you go to <a target="_blank" href="http://localhost:3000/about">http://localhost:3000/about</a>, you should see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728404291897/394ee43b-2b30-43f8-a8cf-ff260d51e82a.png" alt="Image showing how custom pages work " class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>We can also add the page to the navbar by going to the docusaurus.config.js file and adding a new item to the navbar array. The item looks like this:</p>
<pre><code class="lang-json">{to: 'about', label: 'About', position: 'left'},
</code></pre>
<p>It then appears on the homepage nav menu like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728404457186/25545cf0-a5bf-4561-8dc8-49045c3cfc9d.png" alt="Image showing how custom pages work " class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can now style and customize the <code>about.js</code> file any way you’d prefer using React.</p>
<h2 id="heading-styling-in-docusaurus">Styling in Docusaurus</h2>
<p>Let’s look at how you can style your site in Docusaurus. The easiest way is to customize the <code>custom.css</code> file inside the <code>css</code> <strong>&gt;</strong> <code>custom.css</code> file. This is what the file looks like:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728404914713/bd2c8b65-52f9-43d4-b0c8-d09ec9562865.png" alt="Image showing how to perform styling" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Here, you can change the whole color schema of the site and different styling to this file.</p>
<p>You can <strong>read more</strong> about this in the <a target="_blank" href="https://docusaurus.io/docs/styling-layout">Docusaurus styling and layout</a> docs.</p>
<h2 id="heading-seo-in-docusaurus">SEO in Docusaurus</h2>
<p>Docusaurus takes SEO very seriously. By default, Docusaurus automatically adds a description title, canonical URL links, and other useful metadata to each page. This can be configured as shown below:</p>
<pre><code class="lang-markdown">---
title: Our First Page
sidebar<span class="hljs-emphasis">_position: 1

description: A short description of this page
image: ../static/img/docusaurus-social-card.jpg
keywords: [keywords, describing, the main topics]
---

# Single Page

This is a single page.</span>
</code></pre>
<p>You can <strong>read more</strong> about this in the <a target="_blank" href="https://docusaurus.io/docs/seo">Docusaurus SEO</a> docs.</p>
<h2 id="heading-deployment">Deployment</h2>
<p>Deployment is pretty easy with Docusaurus. Since it’s a static site, you can deploy it to any static site hosting service. To do this, run the <code>npm run build</code> command on your CLI. This creates a build folder like the one below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728405905725/a7633e46-cb10-4220-bce8-7b12545a124f.png" alt="Image showing build folder for deployment" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Then, you can upload the contents of the build folder to your hosting service.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this article, we covered how to build documentation from scratch, how to create, customize, and style pages, and the awesome SEO power of Docusaurus.</p>
<p>Docusaurus is friendly to both developers and technical writers. As a developer, you can focus on customizing the site, while as a technical writer, you can focus on writing the documentation.</p>
<p>I will highly recommend this tool for both startups and established enterprises looking to build stunning documentation sites.</p>
<p>This guide is not exhaustive, but covers everything you need to know about the basics of building a documentation site with React and Docusaurus.</p>
<p>I hope you found it helpful :)</p>
<p>Here’s the link to my <a target="_blank" href="https://github.com/ChisomUma/docusaurus-project">GitHub code</a> for follow-up purposes.</p>
<p>And here’s the main Docusaurus <a target="_blank" href="https://docusaurus.io/docs/docs-introduction">documentation</a> if you’d like to dive in deeper.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
