<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chidubem Okoli</title>
    <description>The latest articles on DEV Community by Chidubem Okoli (@duubemmm).</description>
    <link>https://dev.to/duubemmm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2185225%2Ff6ce66b0-eb2d-4bf0-8eab-5b60889078bd.png</url>
      <title>DEV Community: Chidubem Okoli</title>
      <link>https://dev.to/duubemmm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/duubemmm"/>
    <language>en</language>
    <item>
      <title>Building a Custom VPC on AWS with Terraform: Understanding the Architecture</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Wed, 26 Aug 2026 21:03:23 +0000</pubDate>
      <link>https://dev.to/duubemmm/building-a-custom-vpc-on-aws-with-terraform-understanding-the-architecture-46dm</link>
      <guid>https://dev.to/duubemmm/building-a-custom-vpc-on-aws-with-terraform-understanding-the-architecture-46dm</guid>
      <description>&lt;p&gt;A VPC (Virtual Private Cloud) is the foundation for networking in AWS. It provides an isolated virtual network where AWS resources such as EC2 instances, databases, and application servers can communicate with each other and with external services in a controlled way.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://github.com/Duubemmm/terraform-aws-portfolio/tree/main/vpc" rel="noopener noreferrer"&gt;this project&lt;/a&gt;, I provisioned a custom VPC using Terraform and configured public and private subnets, routing, internet connectivity, security controls, an EC2 instance and an S3 Gateway VPC Endpoint.&lt;/p&gt;

&lt;p&gt;More importantly, this project helped me understand how these networking components fit together rather than treating them as isolated AWS resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The infrastructure consists of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A custom VPC using &lt;code&gt;10.0.0.0/16&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A public subnet using &lt;code&gt;10.0.1.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A private subnet using &lt;code&gt;10.0.2.0/24&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Two Availability Zones&lt;/li&gt;
&lt;li&gt;An Internet Gateway&lt;/li&gt;
&lt;li&gt;Separate public and private route tables&lt;/li&gt;
&lt;li&gt;A security group allowing SSH only from my IP&lt;/li&gt;
&lt;li&gt;An EC2 instance in the public subnet&lt;/li&gt;
&lt;li&gt;An S3 Gateway VPC Endpoint connected to the private route table&lt;/li&gt;
&lt;li&gt;DNS support and DNS hostnames enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The architecture can be represented as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         INTERNET
                             │
                             │
                    ┌────────▼────────┐
                    │ Internet Gateway│
                    └────────┬────────┘
                             │
              ┌──────────────┴──────────────┐
              │            VPC              │
              │         10.0.0.0/16         │
              │                             │
              │  ┌───────────────────────┐  │
              │  │    Public Subnet      │  │
              │  │     10.0.1.0/24       │  │
              │  │      eu-west-2c        │  │
              │  │                       │  │
              │  │        EC2             │  │
              │  │         │              │  │
              │  │   Security Group      │  │
              │  └───────────────────────┘  │
              │                             │
              │  ┌───────────────────────┐  │
              │  │   Private Subnet      │  │
              │  │     10.0.2.0/24       │  │
              │  │      eu-west-2b        │  │
              │  │                       │  │
              │  │   Private Route Table │  │
              │  │          │            │  │
              │  │          ▼            │  │
              │  │    S3 Gateway        │  │
              │  │      Endpoint        │  │
              │  └───────────────────────┘  │
              │                             │
              └─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Creating the VPC
&lt;/h2&gt;

&lt;p&gt;The VPC is the top-level network boundary for the infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_support&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;enable_dns_hostnames&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main-vpc"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VPC uses the CIDR block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10.0.0.0/16
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides the address space from which the subnets are created.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;/16&lt;/code&gt; gives the VPC a large address range, while smaller &lt;code&gt;/24&lt;/code&gt; networks can be carved out for individual subnets.&lt;/p&gt;

&lt;p&gt;I also enabled DNS support and DNS hostnames so resources inside the VPC can use AWS-provided DNS functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Dividing the VPC into Subnets
&lt;/h2&gt;

&lt;p&gt;A VPC is a large network, so it is common to divide it into smaller networks called subnets.&lt;/p&gt;

&lt;p&gt;I created two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public subnet:  10.0.1.0/24
Private subnet: 10.0.2.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public subnet is located in &lt;code&gt;eu-west-2c&lt;/code&gt;, while the private subnet is located in &lt;code&gt;eu-west-2b&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_subnet_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2c"&lt;/span&gt;
  &lt;span class="nx"&gt;map_public_ip_on_launch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public-subnet"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_block&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_subnet_cidr&lt;/span&gt;
  &lt;span class="nx"&gt;availability_zone&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"eu-west-2b"&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private-subnet"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A subnet belongs to one Availability Zone.&lt;/p&gt;

&lt;p&gt;Using different Availability Zones also introduces the basic concept of fault isolation: resources can be distributed across physically separate Availability Zones instead of depending entirely on one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What makes a subnet public?
&lt;/h3&gt;

&lt;p&gt;The name &lt;code&gt;public-subnet&lt;/code&gt; does not make a subnet public.&lt;/p&gt;

&lt;p&gt;A subnet becomes public when its associated route table provides a route to an Internet Gateway.&lt;/p&gt;

&lt;p&gt;This distinction is important.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Internet Gateway
&lt;/h2&gt;

&lt;p&gt;The Internet Gateway provides connectivity between the VPC and the internet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_internet_gateway"&lt;/span&gt; &lt;span class="s2"&gt;"gw"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main-igw"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, simply attaching an Internet Gateway to the VPC does not make every subnet public.&lt;/p&gt;

&lt;p&gt;The subnet must have a route that directs internet-bound traffic to the Internet Gateway.&lt;/p&gt;

&lt;p&gt;That brings us to route tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Route Tables
&lt;/h2&gt;

&lt;p&gt;A route table determines where network traffic should be sent.&lt;/p&gt;

&lt;p&gt;For the public subnet, I created a route table with a default route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cidr_block&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
    &lt;span class="nx"&gt;gateway_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_internet_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public-rt"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.0.0.0/0 → Internet Gateway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;0.0.0.0/0&lt;/code&gt; represents all IPv4 destinations.&lt;/p&gt;

&lt;p&gt;Therefore, this route essentially says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If traffic is destined for somewhere outside the VPC, send it toward the Internet Gateway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The public subnet is then associated with this route table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table_association"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The private subnet has a separate route table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_route_table"&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private-rt"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no direct route from this route table to the Internet Gateway.&lt;/p&gt;

&lt;p&gt;Therefore, resources in the private subnet do not have a direct route to the public internet.&lt;/p&gt;

&lt;p&gt;This is what makes the subnet private from a routing perspective.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Public vs Private Communication
&lt;/h2&gt;

&lt;p&gt;One of the most useful things I learned from this project is that "private" does not mean "cannot communicate with anything."&lt;/p&gt;

&lt;p&gt;Both subnets belong to the same VPC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VPC: 10.0.0.0/16
Public subnet:  10.0.1.0/24
Private subnet: 10.0.2.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The VPC has a local route that allows communication within the VPC.&lt;/p&gt;

&lt;p&gt;Therefore, a resource in the private subnet can communicate with a resource in the public subnet using their private IP addresses, assuming security controls allow the traffic.&lt;/p&gt;

&lt;p&gt;The difference is that the private subnet does not have a direct route to the public internet.&lt;/p&gt;

&lt;p&gt;A simple way to think about the components is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route table:&lt;/strong&gt; Where should traffic go?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internet Gateway:&lt;/strong&gt; Provides a path between the VPC and the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Group:&lt;/strong&gt; Is the traffic allowed?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet:&lt;/strong&gt; Which section of the VPC does the resource belong to?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. Security Groups
&lt;/h2&gt;

&lt;p&gt;I created a security group specifically for SSH access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_security_group"&lt;/span&gt; &lt;span class="s2"&gt;"allow_ssh"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow_ssh"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Allow SSH inbound, all outbound"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inbound rule allows TCP traffic on port 22 only from my IP address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_security_group_ingress_rule"&lt;/span&gt; &lt;span class="s2"&gt;"ssh_ipv4"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_ipv4&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_ip&lt;/span&gt;
  &lt;span class="nx"&gt;from_port&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
  &lt;span class="nx"&gt;ip_protocol&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="nx"&gt;to_port&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;22&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My IP is represented as a &lt;code&gt;/32&lt;/code&gt; CIDR, meaning a single IPv4 address.&lt;/p&gt;

&lt;p&gt;This is preferable to opening SSH to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which would allow SSH attempts from any IPv4 address.&lt;/p&gt;

&lt;p&gt;For outbound traffic, I allowed all IPv4 traffic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_security_group_egress_rule"&lt;/span&gt; &lt;span class="s2"&gt;"allow_all_ipv4"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;security_group_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;cidr_ipv4&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.0.0/0"&lt;/span&gt;
  &lt;span class="nx"&gt;ip_protocol&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"-1"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security groups are stateful, meaning return traffic for an allowed connection is automatically permitted.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Launching the EC2 Instance
&lt;/h2&gt;

&lt;p&gt;The EC2 instance is placed inside the public subnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_instance"&lt;/span&gt; &lt;span class="s2"&gt;"main"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ami&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ami_id&lt;/span&gt;
  &lt;span class="nx"&gt;instance_type&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"t2.micro"&lt;/span&gt;
  &lt;span class="nx"&gt;subnet_id&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_security_group_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;aws_security_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allow_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_key_pair&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generated_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key_name&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"main-instance"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the instance is launched, AWS creates a primary Elastic Network Interface (ENI) for it.&lt;/p&gt;

&lt;p&gt;The ENI is essentially the virtual network adapter that connects the EC2 instance to the VPC.&lt;/p&gt;

&lt;p&gt;It is associated with things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The subnet&lt;/li&gt;
&lt;li&gt;A private IP address&lt;/li&gt;
&lt;li&gt;Security groups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The EC2's network interface provides its connection to the VPC and carries its private network identity.&lt;/p&gt;

&lt;p&gt;Because the instance is in the public subnet and the subnet is configured to assign public IPv4 addresses to launched instances, the instance can also receive a public IPv4 address.&lt;/p&gt;

&lt;p&gt;This allows me to connect to it from outside the VPC using SSH, provided the route and security group rules allow the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. S3 Gateway VPC Endpoint
&lt;/h2&gt;

&lt;p&gt;The private subnet doesn't have a direct route to the internet.&lt;/p&gt;

&lt;p&gt;But I still wanted resources in the private subnet to be able to access Amazon S3 without requiring a NAT Gateway.&lt;/p&gt;

&lt;p&gt;For this, I created an S3 Gateway VPC Endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_vpc_endpoint"&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_id&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_vpc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;service_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"com.amazonaws.eu-west-2.s3"&lt;/span&gt;
  &lt;span class="nx"&gt;vpc_endpoint_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Gateway"&lt;/span&gt;
  &lt;span class="nx"&gt;route_table_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;aws_route_table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;tags&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3-gateway-endpoint"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint is associated with the private route table.&lt;/p&gt;

&lt;p&gt;This provides a private path from the VPC to S3 without requiring the private subnet to have general internet connectivity.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Private subnet
      │
      ▼
Private route table
      │
      ▼
S3 Gateway Endpoint
      │
      ▼
Amazon S3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is different from giving the private subnet internet access through a NAT Gateway.&lt;/p&gt;

&lt;p&gt;The subnet remains private while still being able to reach the AWS service it needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. SSH Key Management
&lt;/h2&gt;

&lt;p&gt;For SSH authentication, I used Terraform's TLS provider to generate an RSA private key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"tls_private_key"&lt;/span&gt; &lt;span class="s2"&gt;"ec2_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;algorithm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"RSA"&lt;/span&gt;
  &lt;span class="nx"&gt;rsa_bits&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The corresponding public key is registered with AWS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_key_pair"&lt;/span&gt; &lt;span class="s2"&gt;"generated_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;key_name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"registry-vpc-key"&lt;/span&gt;
  &lt;span class="nx"&gt;public_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tls_private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public_key_openssh&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terraform then writes the private key to a local file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"local_file"&lt;/span&gt; &lt;span class="s2"&gt;"private_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tls_private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ec2_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;private_key_pem&lt;/span&gt;
  &lt;span class="nx"&gt;filename&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/registry-vpc-key.pem"&lt;/span&gt;
  &lt;span class="nx"&gt;file_permission&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0400"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;0400&lt;/code&gt; permission ensures that the private key file is readable only by the file owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Following an SSH Connection Through the Architecture
&lt;/h2&gt;

&lt;p&gt;Putting everything together, an SSH connection from my computer to the EC2 looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;My Computer
     │
     │ TCP 22
     ▼
  Internet
     │
     ▼
Internet Gateway
     │
     ▼
Public Subnet
     │
     ▼
EC2 Network Interface
     │
     ▼
Security Group
     │
     │ Source IP allowed?
     │ TCP 22 allowed?
     ▼
    EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several components have different responsibilities in this flow.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;route table&lt;/strong&gt; provides the path.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Internet Gateway&lt;/strong&gt; provides connectivity between the VPC and the internet.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;network interface&lt;/strong&gt; connects the EC2 to the VPC.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;security group&lt;/strong&gt; determines whether the incoming connection is permitted.&lt;/p&gt;

&lt;p&gt;Understanding these responsibilities makes the architecture much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  11. What I Built
&lt;/h2&gt;

&lt;p&gt;The final infrastructure can be summarized as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS Region: eu-west-2
│
└── VPC: 10.0.0.0/16
    │
    ├── Availability Zone: eu-west-2c
    │   └── Public Subnet: 10.0.1.0/24
    │       ├── Public Route Table
    │       │   └── 0.0.0.0/0 → Internet Gateway
    │       │
    │       └── EC2
    │           └── SSH allowed from my IP
    │
    └── Availability Zone: eu-west-2b
        └── Private Subnet: 10.0.2.0/24
            ├── Private Route Table
            │
            └── S3 Gateway VPC Endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project demonstrates the fundamental building blocks of AWS networking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC → Subnets → Route Tables → Internet Gateway → Security Groups → Network Interfaces → EC2 → VPC Endpoints&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most important lesson for me was understanding that these components are not independent pieces of configuration. They work together to determine &lt;strong&gt;where resources live, where their traffic can go, and which traffic is allowed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This foundation makes it much easier to understand more advanced AWS architectures such as load-balanced applications, private application tiers, NAT Gateways, RDS deployments, ECS, and multi-AZ infrastructure.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>vpc</category>
      <category>devops</category>
    </item>
    <item>
      <title>Deploying a React App to AWS S3 + CloudFront</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Thu, 13 Aug 2026 23:38:59 +0000</pubDate>
      <link>https://dev.to/duubemmm/deploying-a-react-app-to-aws-s3-cloudfront-55hn</link>
      <guid>https://dev.to/duubemmm/deploying-a-react-app-to-aws-s3-cloudfront-55hn</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;CountryRank&lt;/strong&gt; is a React (Vite) app for exploring and comparing countries. I deployed it to AWS using a production-grade static hosting setup: a private S3 bucket serving the build output, sitting behind CloudFront for HTTPS, caching, and correct handling of client-side routing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React + Vite&lt;/strong&gt;: the app itself, built to static assets with &lt;code&gt;npm run build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS S3&lt;/strong&gt;: private bucket storing the build output, no public access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS CloudFront&lt;/strong&gt;: CDN in front of S3, HTTPS, custom error handling for SPA routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS IAM (bucket policy)&lt;/strong&gt;: scoped access control via a service principal and condition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Origin Access Control (OAC)&lt;/strong&gt;: the mechanism that lets CloudFront read from the private bucket&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3erwxtzmqgi829i8kf36.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3erwxtzmqgi829i8kf36.png" alt="AWS architecture diagram showing a React app served through CloudFront, with HTTPS, caching, WAF, and custom error handling. CloudFront accesses a private S3 bucket through Origin Access Control (OAC), with S3 Block Public Access enabled." width="799" height="487"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I built this project in two deliberate stages. I set up plain S3 static hosting first to understand the baseline, then added CloudFront and OAC after seeing the limitations of the S3-only setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: S3 bucket setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the bucket.&lt;/strong&gt; The name has to be globally unique, lowercase and hyphens only. Dots break SSL matching later if CloudFront gets added.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn off Block Public Access&lt;/strong&gt; for this initial public-hosting stage. AWS made me type a confirmation phrase here rather than just unchecking a box, a useful signal that this is a consequential decision and not a routine toggle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable static website hosting&lt;/strong&gt; on the bucket:

&lt;ul&gt;
&lt;li&gt;Index document: &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Error document: &lt;code&gt;index.html&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attach a bucket policy&lt;/strong&gt; granting public read access:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::dubem-country-rank-app/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth internalizing here. &lt;code&gt;Resource&lt;/code&gt; needs &lt;code&gt;/*&lt;/code&gt; because &lt;code&gt;s3:GetObject&lt;/code&gt; acts on individual objects, not the bucket itself; the bucket ARN without &lt;code&gt;/*&lt;/code&gt; refers to the container (its configuration, its listing) rather than what's inside it. And S3 validates the bucket name in the policy against the actual bucket it's attached to, so a typo gets rejected immediately on save.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload the build.&lt;/strong&gt; This is where S3's flat structure matters:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Wrong: nests everything under a dist/ prefix, so index.html isn't found at root&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;dist s3://dubem-country-rank-app/

&lt;span class="c"&gt;# Right: the trailing slash means "copy the contents of dist", not "dist as a unit"&lt;/span&gt;
aws s3 &lt;span class="nb"&gt;sync &lt;/span&gt;dist/ s3://dubem-country-rank-app/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;S3 has no real folders. Everything is a flat list of objects with string keys, and the console just visually groups keys that share a prefix. &lt;code&gt;sync&lt;/code&gt; (as opposed to &lt;code&gt;cp --recursive&lt;/code&gt;) only uploads what's changed, which matters once I'm redeploying repeatedly.&lt;/p&gt;

&lt;p&gt;At this point the site was live on the S3 website endpoint. No server was running anywhere, just object storage answering HTTP requests. But it had real gaps: no HTTPS, no CDN, no custom domain and broken client-side routing. A path like &lt;code&gt;/countries/nigeria&lt;/code&gt; isn't a real object in the bucket, so direct navigation or a refresh returns a 404 unless the error document papers over it. Even then, the response still carries a 404 or 403 status code, which is bad for SEO and monitoring. That's what CloudFront fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: CloudFront setup
&lt;/h2&gt;

&lt;p&gt;Putting CloudFront in front isn't just adding a CDN. It's a structural shift in the trust model. Three things change together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The bucket goes back to &lt;strong&gt;fully private&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;CloudFront gets a dedicated identity via &lt;strong&gt;OAC&lt;/strong&gt; to read from it.&lt;/li&gt;
&lt;li&gt;The bucket policy's principal changes from &lt;code&gt;"*"&lt;/code&gt; to a scoped AWS service principal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Creating the distribution through the guided console flow looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distribution type: &lt;strong&gt;Single website or app&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Origin: selected via &lt;strong&gt;Browse S3&lt;/strong&gt;, which resolves to the bucket's &lt;strong&gt;REST/object endpoint&lt;/strong&gt; (&lt;code&gt;bucket-name.s3.region.amazonaws.com&lt;/code&gt;) rather than the website endpoint&lt;/li&gt;
&lt;li&gt;No custom Route 53 domain for now; CloudFront gives a free &lt;code&gt;*.cloudfront.net&lt;/code&gt; domain by default&lt;/li&gt;
&lt;li&gt;WAF's managed protections left on default (free tier, no cost) rather than customized&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why the REST endpoint and not the website endpoint?&lt;/strong&gt; OAC works by having CloudFront sign its requests to S3, and only the REST API understands signed requests. The website-hosting endpoint is public-only by design and can't validate a signature. Since the bucket is going private anyway, the website endpoint becomes irrelevant at this stage regardless.&lt;/p&gt;

&lt;p&gt;One consequence worth flagging: the REST endpoint has &lt;strong&gt;no concept of index or error documents&lt;/strong&gt;. Everything configured in Step 1's hosting settings doesn't carry over. CloudFront needs its own equivalent settings, covered next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Setting up OAC
&lt;/h2&gt;

&lt;p&gt;Enabling &lt;strong&gt;"Allow private S3 bucket access to CloudFront"&lt;/strong&gt; during distribution creation is the actual OAC toggle. It does two things: it creates an Origin Access Control resource, and it auto-generates a bucket policy update scoped to that resource.&lt;/p&gt;

&lt;p&gt;The generated policy looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AllowCloudFrontServicePrincipal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudfront.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::dubem-country-rank-app/*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"AWS:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:cloudfront::942004241182:distribution/E2XUCTY83EAT31"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Condition&lt;/code&gt; block matters more than it looks. &lt;code&gt;Service: cloudfront.amazonaws.com&lt;/code&gt; alone would let any CloudFront distribution in any AWS account read the bucket. Scoping it to a specific &lt;code&gt;SourceArn&lt;/code&gt; restricts it to this one distribution. Without that, I would just be trading one public-access problem for a slightly narrower one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; enabling OAC updates the bucket policy, but it doesn't touch Block Public Access or remove prior policy statements automatically. That's what bit me next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors I hit and how I fixed them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AccessDenied on the CloudFront root URL
&lt;/h3&gt;

&lt;p&gt;After the distribution deployed, hitting the CloudFront URL returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;Error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Code&amp;gt;&lt;/span&gt;AccessDenied&lt;span class="nt"&gt;&amp;lt;/Code&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Message&amp;gt;&lt;/span&gt;Access Denied&lt;span class="nt"&gt;&amp;lt;/Message&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Error&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I reasoned through it like this: when requesting &lt;code&gt;/&lt;/code&gt;, what object key was CloudFront actually asking S3 for? Nothing in the setup so far told CloudFront that &lt;code&gt;/&lt;/code&gt; should resolve to &lt;code&gt;index.html&lt;/code&gt;. That's a distribution-level setting called &lt;strong&gt;Default Root Object&lt;/strong&gt;, and it isn't inherited from anything configured back in Step 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; in the distribution, go to General, click Edit and set Default Root Object to &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Client-side routes still broken after that
&lt;/h3&gt;

&lt;p&gt;Fixing the root didn't fix direct navigation to &lt;code&gt;/countries/nigeria&lt;/code&gt;. Same underlying problem as the S3-only stage, still unresolved, because Default Root Object only handles the exact-root case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Custom Error Responses on the distribution:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;HTTP error code&lt;/th&gt;
&lt;th&gt;Response page path&lt;/th&gt;
&lt;th&gt;HTTP response code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;404&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/index.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both codes are needed because a private bucket accessed via OAC tends to return 403 for a missing key rather than a clean 404. It doesn't distinguish "doesn't exist" from "not allowed to see it," it just denies. This is the fix plain S3 hosting couldn't offer: CloudFront can rewrite the status code itself, not just swap in different content, so a deep-linked SPA route now returns a legitimate 200 while React Router takes over client-side.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Old public bucket policy left stacked on top of the new one
&lt;/h3&gt;

&lt;p&gt;After everything was working, I went back and checked the bucket policy directly instead of assuming the wizard had cleaned up after itself. I found the original &lt;code&gt;Principal: "*"&lt;/code&gt; statement from Step 1 still sitting there, alongside the new CloudFront-scoped one. Block Public Access was also still off.&lt;/p&gt;

&lt;p&gt;That meant the bucket was reachable two ways: through CloudFront (secured, cached, HTTPS) and directly via the old public endpoint (none of that), quietly defeating the point of the whole migration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Removed the old &lt;code&gt;Principal: "*"&lt;/code&gt; statement, keeping only the CloudFront-scoped one.&lt;/li&gt;
&lt;li&gt;Re-enabled Block Public Access.&lt;/li&gt;
&lt;li&gt;Verified by testing both URLs: the CloudFront URL still worked, and the direct S3 endpoint failed. That's the actual proof the bucket is only reachable through the intended path.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;S3 access control is layered: Block Public Access is the master override, the bucket policy is the grant, and the resource ARN scope decides bucket versus object. All three have to align, and missing any one breaks things in a specific, diagnosable way.&lt;/li&gt;
&lt;li&gt;S3 "folders" are prefixes, not real directories. This affects upload commands directly, not just how the console displays things.&lt;/li&gt;
&lt;li&gt;Website-hosting endpoints and REST endpoints are genuinely different features with different capabilities. OAC only works with the REST one.&lt;/li&gt;
&lt;li&gt;CloudFront doesn't inherit S3's static-hosting settings. Default Root Object and Custom Error Responses have to be configured independently, and skipping them produces specific, traceable errors rather than vague failures.&lt;/li&gt;
&lt;li&gt;Auto-generated policies from console wizards can leave old, conflicting statements behind. It's worth checking the actual policy JSON manually rather than assuming the tool fully cleaned up after itself.&lt;/li&gt;
&lt;li&gt;Debugging AccessDenied is faster when reasoning through which layer is missing (policy, public access block, or routing config) rather than guessing and re-toggling settings at random.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>s3</category>
      <category>cloudfront</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Set Up SSH Keys on Windows: A Step-by-Step Guide</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Tue, 02 Sep 2025 08:08:57 +0000</pubDate>
      <link>https://dev.to/duubemmm/set-up-ssh-keys-on-windows-a-step-by-step-guide-43d5</link>
      <guid>https://dev.to/duubemmm/set-up-ssh-keys-on-windows-a-step-by-step-guide-43d5</guid>
      <description>&lt;p&gt;Secure Shell (SSH) is a security protocol that creates a highly secure connection between your computer and GitHub. It uses a pair of cryptographic keys—a public key you share and a private key you keep safe—to verify your identity.&lt;/p&gt;

&lt;p&gt;The main benefit of using SSH keys is convenience and security. Once set up, you can connect to GitHub to push and pull code without having to enter your username and personal access token every time.&lt;/p&gt;

&lt;p&gt;Here's a step-by-step guide on how to set up your SSH keys in a Windows environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Check for Existing SSH Keys&lt;/strong&gt;&lt;br&gt;
First, let's see if you already have an SSH key pair. It's good practice to avoid creating new ones if you don't have to.&lt;/p&gt;

&lt;p&gt;a) Open &lt;strong&gt;PowerShell&lt;/strong&gt; (press the Windows key, type PowerShell, and press Enter).&lt;/p&gt;

&lt;p&gt;b) The default directory is /.ssh. Run the following command to list files in the .ssh directory, which is where keys are typically stored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -a ~/.ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;c) Look for a pair of files named something like id_rsa and id_rsa.pub, or id_ed25519 and id_ed25519.pub. If you see a .pub file, you already have a key and can skip to &lt;strong&gt;Step 3&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Alternatively, you can get the key by typing into the powershell terminal&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Generate a New SSH Key Pair&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you don't have an existing key, you'll need to create one. We'll generate a modern and secure Ed25519 key.&lt;/p&gt;

&lt;p&gt;a) In your PowerShell terminal, run the following command. Make sure to replace "&lt;a href="mailto:your_email@example.com"&gt;your_email@example.com&lt;/a&gt;" with the email address associated with your GitHub account.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen -t ed25519 -C "your_email@example.com"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) The command will prompt you to choose a location to save the key. The default location (~/.ssh/id_ed25519) is perfect, so just press Enter.&lt;/p&gt;

&lt;p&gt;c) Next, you'll be asked to enter a passphrase. 🔐 &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A passphrase adds an extra layer of security; if someone gains access to your computer, they still can't use your key without knowing the passphrase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's highly recommended to use one, but you can leave it blank by pressing Enter twice.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Enter passphrase (empty for no passphrase): [Type a passphrase]
&amp;gt; Enter same passphrase again: [Type passphrase again]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You've now created two files in your ~/.ssh directory: id_ed25519 (your private key) and id_ed25519.pub (your public key). Never share your private key!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Add Your SSH Key to the ssh-agent&lt;/strong&gt;&lt;br&gt;
The ssh-agent is a background program that securely stores your private key and manages its passphrase so you don't have to re-enter it constantly.&lt;/p&gt;

&lt;p&gt;a) First, ensure the ssh-agent service is running. This command will start it if it isn't active.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start-SshAgent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) Now, add your private SSH key to the agent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-add ~/.ssh/id_ed25519
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;c) If you created a passphrase in the previous step, you will be prompted to enter it now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Add Your Public SSH Key to GitHub&lt;/strong&gt;&lt;br&gt;
Now you need to tell GitHub about your public key so it can grant you access.&lt;br&gt;
a) Now you need to copy your public SSH key. To do this, we’re going to use a command called cat to read the file to the console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat ~/.ssh/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) Open your browser and navigate to GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Click your profile picture in the top-right corner, then click Settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the left sidebar, click SSH and GPG keys.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;c) Click the New SSH key button.&lt;/p&gt;

&lt;p&gt;d) In the Title field, give your key a descriptive name, like "My Windows Laptop" or "Work PC".&lt;/p&gt;

&lt;p&gt;e) Click inside the Key field and paste your key by pressing Ctrl + V.&lt;/p&gt;

&lt;p&gt;f) Finally, click Add SSH key. You may be asked to enter your GitHub password to confirm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Test Your SSH Connection&lt;/strong&gt; &lt;br&gt;
Let's make sure everything is working correctly.&lt;/p&gt;

&lt;p&gt;a) Back in your PowerShell terminal, run this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -T git@github.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;b) You may see a warning about the authenticity of the host. This is normal for the first connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The authenticity of host 'github.com (IP ADDRESS)' can't be established.
ED25519 key fingerprint is SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Type yes and press Enter.&lt;/p&gt;

&lt;p&gt;c) If everything is set up correctly, you'll see a welcome message with your username! 🎉&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi your-username! You've successfully authenticated, but GitHub does not provide shell access.

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

&lt;/div&gt;



&lt;p&gt;You're all set! You can now clone, pull, and push to your GitHub repositories using SSH without needing to enter your credentials every time.&lt;/p&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Fixed Firebase’s “Unauthorized Domain” Error While Building Resumpire</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Wed, 09 Jul 2025 14:52:58 +0000</pubDate>
      <link>https://dev.to/duubemmm/how-i-fixed-firebases-unauthorized-domain-error-while-building-resumpire-2567</link>
      <guid>https://dev.to/duubemmm/how-i-fixed-firebases-unauthorized-domain-error-while-building-resumpire-2567</guid>
      <description>&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64ke1z161s0dp8hqt7lv.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64ke1z161s0dp8hqt7lv.jpg" alt="Firebase Error" width="800" height="385"&gt;&lt;/a&gt;&lt;br&gt;
I was deep into building &lt;a href="//resumpire.vercel.app"&gt;resumpire.vercel.app&lt;/a&gt; (a resume builder app) when I hit a wall. Firebase kept rejecting my authentication requests with an “Unauthorized Domain” error.&lt;/p&gt;

&lt;p&gt;At first, I was running through all the usual suspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Maybe I broke something in my codebase?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Could there be a dependency version conflict?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Is there a typo in my config?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After scouring my codebase trying to find the bug, I finally spotted the issue.&lt;br&gt;
Turns out, the issue was much simpler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Culprit: Firebase’s Authorized Domain List&lt;/strong&gt;&lt;br&gt;
Firebase Authentication only allows requests from domains you explicitly authorize. That means:&lt;/p&gt;

&lt;p&gt;If you’re testing locally, you must add &lt;a href="http://localhost" rel="noopener noreferrer"&gt;http://localhost&lt;/a&gt; (or your custom dev URL) to Firebase’s Authorized Domains list.&lt;/p&gt;

&lt;p&gt;If you’re deploying, your live domain (e.g., resumpire.vercel.app) needs to be added too.&lt;/p&gt;

&lt;p&gt;My deployed domain resumpire.vercel.app wasn't in the Firebase Authorized Domains list. I'd only added localhost during development.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I Fixed It&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Opened Firebase Console → Authentication → Settings&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scrolled to “Authorized Domains”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added my production domain (resumpire.vercel.app)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reloaded the app → Auth worked instantly&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson learned: Always check the docs (and the obvious settings) first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why This Matters&lt;/strong&gt;&lt;br&gt;
This wasn’t just about fixing an error—it reinforced a bigger lesson:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Small oversights cause big blockers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reading docs &amp;gt; guessing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every error is an avenue to learn and grow which makes you a better dev.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How I Built an AI-Powered Text Processor Using Chrome AI API.</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Thu, 27 Mar 2025 08:40:36 +0000</pubDate>
      <link>https://dev.to/duubemmm/how-i-built-an-ai-powered-text-processor-using-chrome-ai-api-1j0f</link>
      <guid>https://dev.to/duubemmm/how-i-built-an-ai-powered-text-processor-using-chrome-ai-api-1j0f</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;When I first discovered the Chrome AI API, I recognized its potential immediately. The ability to integrate AI-powered text processing directly into a web app—without requiring a backend—opens up new possibilities for seamless user experiences. Using React and TailwindCSS, I built a practical AI Text Processor capable of summarizing, translating, and detecting languages entirely in the browser.&lt;/p&gt;

&lt;p&gt;In this guide, I’ll walk you through:&lt;br&gt;
✔ Why the Chrome AI API is a game-changer&lt;br&gt;
✔ Step-by-step setup (including troubleshooting tips)&lt;br&gt;
✔ How I built the UI with React &amp;amp; TailwindCSS&lt;br&gt;
✔ Lessons learned and where to go next&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup &amp;amp; Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before diving in, you’ll need:&lt;/p&gt;

&lt;p&gt;1) Comply with Google’s AI Policies&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure your use case follows Google’s Generative AI Prohibited Use Policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;2) Install Chrome Canary&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download Chrome Canary (v129.0.6639.0 or newer).
Why Canary? The API is experimental and only available here for now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;3) Check Storage Requirements&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimum 22GB free storage. If storage drops below 10GB, Chrome automatically deletes the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4) Enable the Summarization API&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open chrome://flags/#summarization-api-for-gemini-nano&lt;/li&gt;
&lt;li&gt;Select Enabled&lt;/li&gt;
&lt;li&gt;Relaunch Chrome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;5) Force-Download the AI Model&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open DevTools (F12) and run:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;await ai.summarizer.create(); // Triggers model download&lt;br&gt;
await ai.summarizer.capabilities(); // Check status&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Repeat the second command until it returns "readily" (~3-5 mins).&lt;/p&gt;

&lt;p&gt;Stuck? If you see:&lt;/p&gt;

&lt;p&gt;"The model was available but there was no execution config available for the feature."&lt;/p&gt;

&lt;p&gt;Wait 24 hours and retry (Google rolls out access gradually).&lt;/p&gt;

&lt;p&gt;Troubleshooting Tip: For full documentation, check Chrome’s AI API Guide &lt;a href="https://docs.google.com/document/d/1bzpeKk4k26KfjtR-_d9OuXLMpJdRMiLZAOVNMuFIejk/edit?tab=t.0" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Chrome AI API?
&lt;/h2&gt;

&lt;p&gt;This API brings serverless AI to the browser with:&lt;br&gt;
✅ No backend needed – Runs entirely client-side.&lt;br&gt;
✅ Blazing fast – Responses in milliseconds.&lt;br&gt;
✅ Privacy-focused – No data sent to third-party servers.&lt;br&gt;
✅ Easy integration – Just call ai. methods like translate() or summarize().&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Features
&lt;/h2&gt;

&lt;p&gt;The AI Text Processor includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Text Input - A responsive text area for content entry&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Language Detection - Automatic identification of input text language&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Text Summarization - Concise distillation of key information&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multilingual Translation - Conversion between supported languages&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Accessible Interface - Screen reader compatible with keyboard navigation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Error Handling - Graceful failure modes and user feedback&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;React – UI components &amp;amp; state management.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TailwindCSS – Rapid styling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Chrome AI API – ai.summarizer, ai.translator, ai.detector.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Building the App
&lt;/h2&gt;

&lt;p&gt;1) Initialize the Project&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm create vite@latest ai-text-processor --template react&lt;br&gt;
cd ai-text-processor&lt;br&gt;
npm install&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;2) Add TailwindCSS&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install -D tailwindcss postcss autoprefixer&lt;br&gt;
npx tailwindcss init -p&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Configure tailwind.config.js:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;export default {&lt;br&gt;
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],&lt;br&gt;
  theme: { extend: {} },&lt;br&gt;
  plugins: [],&lt;br&gt;
};&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Add to index.css:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@tailwind base;&lt;br&gt;
@tailwind components;&lt;br&gt;
@tailwind utilities;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;3) Create the UI&lt;br&gt;
A simple layout with:&lt;/p&gt;

&lt;p&gt;A textarea for input.&lt;/p&gt;

&lt;p&gt;Buttons for actions (Summarize/Translate/Detect).&lt;/p&gt;

&lt;p&gt;A results panel (conditional rendering with error handling).&lt;/p&gt;

&lt;p&gt;4) Integrate the Chrome AI API&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const handleSummarize = async () =&amp;gt; {&lt;br&gt;
  try {&lt;br&gt;
    const summary = await ai.summarizer.summarize(text);&lt;br&gt;
    setResult(summary);&lt;br&gt;
  } catch (error) {&lt;br&gt;
    setResult("Error: " + error.message);&lt;br&gt;
  }&lt;br&gt;
};&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;More languages – Expand translation support.&lt;/p&gt;

&lt;p&gt;Offline mode – Cache results for poor connectivity.&lt;/p&gt;

&lt;p&gt;Custom prompts – Let users tweak summarization style.&lt;/p&gt;

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

&lt;p&gt;The Chrome AI API opens up exciting possibilities for browser-based AI apps. While still experimental, it’s a glimpse into a future where frontend devs can leverage AI without backend complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the demo &amp;amp; code
&lt;/h2&gt;

&lt;p&gt;GitHub Repo Link &lt;a href="https://github.com/Duubemmm/hng12-stage3-AI-Text-Processing-App" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
Live Link &lt;a href="https://hng12-stage3-ai-text-processing-app.vercel.app/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Note: This implementation is currently available only for desktop platforms&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>javascript</category>
      <category>api</category>
      <category>ai</category>
    </item>
    <item>
      <title>Recreating the Interswitch Homepage with React and TailwindCSS.</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Tue, 14 Jan 2025 13:24:30 +0000</pubDate>
      <link>https://dev.to/duubemmm/recreating-the-interswitch-homepage-with-react-and-tailwindcss-2ppf</link>
      <guid>https://dev.to/duubemmm/recreating-the-interswitch-homepage-with-react-and-tailwindcss-2ppf</guid>
      <description>&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recreating a modern, visually appealing landing page is always an exciting challenge. This week, I focused on building a replica of the Interswitch homepage using React and TailwindCSS. This article provides a technical walkthrough of the process, from project setup to implementing reusable components and styling. Here's how I approached it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project Setup with Vite&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vite has become my go-to tool for React projects due to its blazing-fast build times and simplicity. The setup process involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm create vite@latest interswitch-clone --template react
cd interswitch-clone
npm install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the development server running, I was ready to start coding.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structuring Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Breaking the homepage into reusable components was essential for maintainability and scalability. Below are a few key components I implemented. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NavBar Component&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { useState } from "react";
import { FaBars, FaTimes } from "react-icons/fa";
import { FiChevronDown } from "react-icons/fi";

const Navbar = () =&amp;gt; {
  const [isOpen, setIsOpen] = useState(false);
  const [dropdownOpen, setDropdownOpen] = useState(false);

  const navLinks = [
    { title: "About Us", hasDropdown: true },
    { title: "What We Do", hasDropdown: true },
    { title: "Financial Inclusion", hasDropdown: false },
    { title: "Corporate Responsibility", hasDropdown: false },
    { title: "News &amp;amp; Insights", hasDropdown: false },
  ];

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Stats Component&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const Stats = () =&amp;gt; {
  return (
    &amp;lt;div className="bg-blue-50 py-12"&amp;gt;
      &amp;lt;div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"&amp;gt;
        &amp;lt;div className="flex flex-col gap-8"&amp;gt;
          &amp;lt;div className="flex flex-col md:flex-row items-start gap-8"&amp;gt;
            &amp;lt;h2 className="text-3xl md:text-4xl font-semibold text-gray-900 flex-1"&amp;gt;
              Pushing the boundaries of innovation to deliver payment solutions that enable commerce across Africa
            &amp;lt;/h2&amp;gt;
            &amp;lt;div className="flex-1 flex flex-col gap-4"&amp;gt;
              &amp;lt;p className="text-xl text-gray-700"&amp;gt;
                Bespoke payment solutions for your modern lifestyle, business collections, disbursements, and payment processing.
              &amp;lt;/p&amp;gt;
              &amp;lt;button className="bg-blue-950 text-white px-6 py-3 rounded-md hover:bg-blue-900 transition w-fit"&amp;gt;
                Learn More
              &amp;lt;/button&amp;gt;
            &amp;lt;/div&amp;gt;
          &amp;lt;/div&amp;gt;
export default Stats;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Styling with TailwindCSS&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;TailwindCSS made styling the components seamless. By leveraging utility classes, I could focus on functionality without writing custom CSS. For example, the hero section below uses Tailwind’s gradient and typography utilities to create an eye-catching design.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const Hero = () =&amp;gt; {
  return (
    &amp;lt;div className="text-blue-950 pt-6 relative"&amp;gt;
      &amp;lt;div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"&amp;gt;
        &amp;lt;div className="grid md:grid-cols-2 gap-12 items-center"&amp;gt;
          &amp;lt;div&amp;gt;
            &amp;lt;h1 className="text-2xl md:text-7xl mb-6 mt-16 font- text-blue-950"&amp;gt;
              The Gateway To Africa&amp;amp;apos;s Payment Ecosystem
            &amp;lt;/h1&amp;gt;
            &amp;lt;p className="text-xl md:text-xl mb-8 text-black-200"&amp;gt;
              We create and sustain a payment ecosystem that helps 
  commmerce evolve, businesses grow and individuals thrive.
            &amp;lt;/p&amp;gt;
          &amp;lt;/div&amp;gt;
       &amp;lt;/div&amp;gt;
     &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;


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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Componentization: Breaking the UI into reusable components ensured better maintainability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;TailwindCSS: The utility-first approach made styling intuitive and efficient.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Vite: Its fast build times improved the development experience.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Recreating the Interswitch homepage was a rewarding experience that solidified my understanding of React and TailwindCSS. By leveraging modern tools and best practices, I built a scalable and visually appealing landing page. If you’re working on a similar project or have questions, let’s connect in the comments!&lt;/p&gt;

</description>
      <category>react</category>
      <category>frontend</category>
      <category>tailwindcss</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Elevate Your JavaScript Journey: Tackling Projects and Unlocking Features 🚀</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Thu, 05 Dec 2024 17:04:36 +0000</pubDate>
      <link>https://dev.to/duubemmm/elevate-your-javascript-journey-tackling-projects-and-unlocking-features-3mk9</link>
      <guid>https://dev.to/duubemmm/elevate-your-javascript-journey-tackling-projects-and-unlocking-features-3mk9</guid>
      <description>&lt;p&gt;JavaScript is an ever-evolving journey where each project adds a new layer to your skills. Whether you're building a ticketing system or adding functionality to a to-do list, there's always something new to learn! Let me walk you through my recent experiments, lessons, and insights.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building a Ticketing System 🎟️&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When working on a ticket system, I wanted to level it up by allowing users to input any number of tickets instead of relying on predefined buttons. The goal was simple: improve flexibility while keeping the system intuitive.&lt;/p&gt;

&lt;p&gt;Here’s what I learned:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DOM Manipulation Is Key&lt;/strong&gt;: Capturing input values dynamically and using them effectively in your JavaScript logic is vital.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Validations Matter&lt;/strong&gt;: When you let users enter values, ensure you're checking for edge cases like negative numbers or empty inputs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Iterate &amp;amp; Improve&lt;/strong&gt;: Building on the foundational ticket counter I developed earlier, I realized how important it is to refactor code for scalability.&lt;br&gt;
If you're considering a project like this, start small. Add basic functionality, then challenge yourself to build on top of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding Delete Functionality to a To-Do List ✔️❌&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To-do lists are a staple project for beginners—and for good reason! They offer a practical way to learn about adding and removing elements from the DOM. Recently, I wanted to add a delete button to each item dynamically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Approach&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generate Buttons on Item Creation&lt;/strong&gt;: When the user adds a new task, a delete button is created alongside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Event Listeners Dynamically&lt;/strong&gt;: I attached an event listener to each button to handle the deletion logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parent-Child DOM Interaction&lt;/strong&gt;: Using parentElement, I ensured clicking the delete button would remove its corresponding list item.&lt;br&gt;
Here’s a snippet of the core idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function addTask(taskText) {

  const li = document.createElement("li");
  li.textContent = taskText;

  const deleteBtn = document.createElement("button");
  deleteBtn.textContent = "Delete";


  deleteBtn.addEventListener("click", () =&amp;gt; {
    li.remove();
  });

  li.appendChild(deleteBtn);
  document.querySelector("#task-list").appendChild(li);
}

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

&lt;/div&gt;



&lt;p&gt;With this approach, I learned how important it is to structure your DOM interactions cleanly and avoid cluttered logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lessons Learned 🧠&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.Experimentation Drives Learning&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Projects like these reinforce that theory is nothing without practice. By experimenting with features, I gained a deeper understanding of DOM manipulation, event listeners, and JavaScript logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.Simple Features, Big Impact&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The smallest changes—like a delete button—can significantly enhance your project's user experience. Don't underestimate the value of small wins!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3.Learn in Layers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Building on previous projects is the best way to reinforce your skills. I started with a basic ticket counter, then expanded it. Similarly, I took my to-do list from "add-only" to "add-and-delete."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's Next?🔮&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JavaScript is all about iteration. I’m excited to explore more advanced concepts like input validations, animations, and even local storage to persist data between sessions.&lt;/p&gt;

&lt;p&gt;If you're on a similar journey, start small and stay consistent. Every project you complete is another step forward.&lt;/p&gt;

&lt;p&gt;Let me know in the comments—what’s your favorite beginner-friendly JavaScript project?&lt;/p&gt;

&lt;p&gt;Happy coding! 🚀&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Installing And Running NPM Packages Using The Terminal</title>
      <dc:creator>Chidubem Okoli</dc:creator>
      <pubDate>Mon, 04 Nov 2024 08:57:52 +0000</pubDate>
      <link>https://dev.to/duubemmm/installing-and-running-npm-packages-using-the-terminal-1i09</link>
      <guid>https://dev.to/duubemmm/installing-and-running-npm-packages-using-the-terminal-1i09</guid>
      <description>&lt;p&gt;When working on a JavaScript project, chances are you'll need to install and use npm (Node Package Manager) packages. &lt;/p&gt;

&lt;p&gt;npm is essential for managing libraries and tools that enhance your development process. If you're new to this, here's a step-by-step guide to installing and running npm packages using the terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;br&gt;
Install Node.js: Ensure that the recent version of Node.js is installed on your machine, as npm comes bundled with it. You can check if it's installed by running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;node -v&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm -v&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If you don't have it installed, download it from &lt;a href="https://nodejs.org/https://nodejs.org/" rel="noopener noreferrer"&gt;https://nodejs.org/https://nodejs.org/&lt;/a&gt; and follow the installation instructions.&lt;/p&gt;

&lt;p&gt;After confirming it is successfully installed, create a folder for your project or navigate to an existing one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialize The Project&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To start using npm, you need a package.json file, which tracks the project's dependencies and scripts. You can create one by running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;npm init&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;You’ll be prompted with some basic questions (like project name, version, description, etc.). You can either answer each or press Enter to accept the defaults.&lt;br&gt;
Alternatively, you can use &lt;code&gt;npm init -y&lt;/code&gt; to create a default package.json file without prompts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Installing npm Packages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With npm, you can install packages either globally or locally to your project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local Installation: Installs the package in the project directory and adds it to your package.json.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm install package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This installs the package locally. You'll find it under the node_modules folder, and it will be listed in your package.json file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global Installation: Installs the package globally on your system, making it available from any directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;npm install -g package-name&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This installs the package globally, allowing you to run the package commands directly in any project without needing to install it locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Using npm Scripts to Run Commands&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Scripts defined in package.json can simplify common tasks like running tests or starting the server. In your package.json, there’s a "scripts" section where you can define custom commands.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
"scripts": {&lt;br&gt;
  "start": "node index.js",&lt;br&gt;
  "test": "echo \"Error: no test specified\" &amp;amp;&amp;amp; exit 1"&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
In this example:&lt;/p&gt;

&lt;p&gt;"start" will run "node index.js".&lt;br&gt;
You can replace "test" or "start" with other custom scripts.&lt;/p&gt;

&lt;p&gt;To run a script, use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm run script-name&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running Installed Packages from the Terminal&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you installed a package globally, you can use it directly in the terminal.&lt;br&gt;
For locally installed packages, use npx, which comes bundled with npm:&lt;br&gt;
&lt;code&gt;npx package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx create-react-app my-app&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This command runs the create-react-app package without needing to install it globally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Updating and Removing Packages&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To update a package, use:&lt;br&gt;
&lt;code&gt;npm update package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
To remove a package, use:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm uninstall package-name&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
This command will remove the package from the node_modules folder and from package.json.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
By understanding these basic npm commands, you can confidently install, run, and manage packages in any Node.js project.&lt;/p&gt;

</description>
      <category>node</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
