Build a Company Stock Tracker

Build a live stock tracker app using the Finnhub API and deploy it on Vercel.

Introduction

30 Second Summary

Ever wondered how stock tracking apps pull live company data the moment you search for a ticker? Behind the scenes, they make calls to financial data providers and update the page without a reload.

In this project, you will build a Company Stock Tracker that fetches real financial data from the Finnhub API. You'll deploy it live on the internet via Vercel so anyone can use it.

What You'll Build

A responsive single-page web app that searches for publicly traded companies by ticker symbol and displays their profile, real-time stock price, and key statistics.

By the end of this project, you'll have:

  • A working stock tracker web app that fetches real company data from the Finnhub API.
  • Hands-on experience with async JavaScript patterns (fetch, async/await, Promise.all) used in every professional frontend role.
  • A live deployed app on Vercel via GitHub that you can share with anyone.
  • Secret Mission: A watchlist feature that saves multiple stocks and displays them as a comparison dashboard.

Are there any prerequisites?

This project assumes a basic understanding of HTML, CSS, and JavaScript. No prior experience with APIs or deployment is needed.

Before We Start

Before you write any code, you need a few tools in place.

In this step, you will install Git for version control, grab a free API key from Finnhub to pull live stock data, and add the Live Server extension to VS Code so you can preview your app with auto-refresh.

In this step, get ready to:
  • Install Git and verify it in your terminal.
  • Get a free Finnhub API key.
  • Install the Live Server extension in VS Code.
Install Git

Git is the version control tool you will use to track your code and push it to GitHub later in the project.

  • Open your browser and go to https://git-scm.com.
  • Click the download button for your operating system and run the installer.

What should I expect during installation?

The Git installer shows several configuration screens. Accept all the default options by clicking Next through each screen until you reach Install. No custom settings are needed for this project.

  • Once installation finishes, switch back to VS Code.
  • Open a new terminal by pressing Ctrl+` (Windows) or Cmd+` (macOS).
  • Verify Git is installed by running this command:
git --version

✔️ I see a version number

Git is ready to go. Move on to the next substep.

ⓧ Command not found

Your terminal does not recognize git yet. Try these fixes:

  • Close your VS Code terminal and open a fresh one. Git registers itself on your system PATH during installation, but existing terminals do not pick up PATH changes.
  • If a fresh terminal still does not work, restart VS Code entirely, then open a new terminal and run the command again.
  • On macOS, running git --version may prompt you to install Xcode Command Line Tools. Follow the on-screen prompt to install, then run the command again.

Still not working?

Re-download the installer from https://git-scm.com and run it again. Make sure you complete the full installation before checking.

Help me troubleshoot why git --version is not recognized in my VS Code terminal

Get your Finnhub API key

Finnhub is a free financial data API that provides real-time stock quotes and company profiles. Your app will call Finnhub to fetch live data.

  • Open https://finnhub.io in your browser.
  • Click Get free API key.
  • Register with your email address. No credit card is required.
  • After registering, you will be taken to your dashboard. Copy the API key shown there and save it somewhere safe (a text file or sticky note on your desktop works fine).

Is my API key safe in client-side code?

For this learning project, embedding the key directly in your JavaScript file is acceptable. The Finnhub free tier only provides read access to public financial data, so there is no security risk.

In a production application you would proxy API calls through a backend server to keep keys private. For now, the simpler approach lets you focus on learning Fetch API patterns.

Not receiving the confirmation email?

Check your spam or junk folder. If it still does not appear after a few minutes, try registering with a different email address.

Help me sign up for a free Finnhub API key

Install the Live Server extension

Live Server launches a local development server that auto-refreshes your browser every time you save a file. This means you will see your changes instantly without manually reloading.

  • In VS Code, open the Extensions view by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Type Live Server in the search box.
  • Find the extension published by Ritwick Dey in the results list.
  • Click the Install button.

Once installed, the Install button changes to a gear icon. You are all set.

Can't find Live Server in the results?

Make sure you are searching in the Extensions view (the puzzle-piece icon in the left sidebar), not the file search bar at the top. If multiple results appear, look for the one by Ritwick Dey with millions of downloads.

Help me find and install the Live Server extension in VS Code

Your tools are ready. Next up, you will create your project files and build the HTML structure for the stock tracker.

Build the HTML Structure

Your tools are installed and your API key is ready. Now it's time to build the actual app.

Every web app starts with an HTML file that defines the structure of what users see. In this step, you'll create the skeleton of your stock tracker: a search bar for entering ticker symbols, areas for loading and error states, and a card that displays the stock data.

In this step, get ready to:
  • Create a project folder and three starter files.
  • Build the full HTML page with a search form, loading spinner, error message, and stock card.
  • Preview the app in your browser using Live Server.
Create the project folder and files

Before writing any code, you need a dedicated folder to hold your project files. You'll create this on your Desktop so it's easy to find.

  • In VS Code, open your terminal by pressing Ctrl+` (backtick key, usually above Tab).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir stock-tracker
cd stock-tracker
  • Open this folder in VS Code by clicking File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the stock-tracker folder, and click Open.

Why open the folder this way?

Live Server requires a folder to be opened in VS Code (not just a single file). Opening the folder now ensures Live Server works correctly when you preview your app later.

  • VS Code may ask if you trust the authors of files in this folder. Click Yes, I trust the authors.
  • In the Explorer sidebar on the left, hover over the STOCK-TRACKER heading and click the New File icon (it looks like a page with a plus sign).
  • Type index.html and press Enter.
  • Repeat to create a second file called style.css.
  • Repeat to create a third file called script.js.
  • Confirm your files exist by opening the terminal in VS Code and running:
ls

You should see index.html, style.css, and script.js in the output.

Don't see all three files?

Make sure you created each file at the top level of the stock-tracker folder, not inside a subfolder. Check the Explorer sidebar to confirm all three are listed directly under STOCK-TRACKER.

Help me find my missing files in VS Code.

Build the HTML page

Your index.html file is the backbone of your app. It defines every section the user will see: a search bar at the top, a loading spinner, an error message area, and a stock card that displays company data.

  • Click on index.html in the Explorer sidebar to open it in the editor.
  • Paste the following code into the file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Stock Tracker</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>📈 Stock Tracker</h1>
            <p>Search any publicly traded company</p>
        </header>

        <div class="search-section">
            <form id="search-form">
                <input
                    type="text"
                    id="symbol-input"
                    placeholder="Enter ticker symbol (e.g., AAPL)"
                    autocomplete="off"
                    required
                >
                <button type="submit" id="search-btn">Search</button>
            </form>
        </div>

        <div id="loading" class="loading hidden">
            <div class="spinner"></div>
            <p>Fetching data...</p>
        </div>

        <div id="error-message" class="error hidden"></div>

        <div id="stock-card" class="stock-card hidden">
            <div class="company-header">
                <img id="company-logo" src="" alt="Company logo">
                <div class="company-info">
                    <h2 id="company-name"></h2>
                    <p id="company-details"></p>
                </div>
            </div>

            <div class="price-section">
                <div class="current-price">
                    <span id="stock-price"></span>
                    <span id="price-change"></span>
                </div>
            </div>

            <div class="stats-grid">
                <div class="stat">
                    <span class="stat-label">Open</span>
                    <span id="stat-open" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">High</span>
                    <span id="stat-high" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Low</span>
                    <span id="stat-low" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Prev Close</span>
                    <span id="stat-prev-close" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Market Cap</span>
                    <span id="stat-market-cap" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Industry</span>
                    <span id="stat-industry" class="stat-value"></span>
                </div>
            </div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

What does this HTML do?

  • The <form id="search-form"> creates the search bar where users type a ticker symbol like AAPL or TSLA.
  • The <div id="loading"> holds a spinner animation shown while data loads from the API.
  • The <div id="error-message"> is where error text appears if something goes wrong.
  • The <div id="stock-card"> is the main display area with the company logo, stock price, and six stats (open, high, low, previous close, market cap, industry).
  • The hidden class on loading, error, and stock-card means they start invisible. JavaScript will reveal them at the right time.
  • Save index.html by pressing Ctrl+S (Windows) or Cmd+S (macOS).

How are the CSS and JS files connected?

The <link rel="stylesheet" href="style.css"> tag in the head section tells the browser to load your stylesheet. The <script src="script.js"></script> tag at the bottom of the body loads your JavaScript. Both files are empty right now, but the connections are already wired up.

Preview your app with Live Server

Even though the page has no styling yet, you can verify your HTML structure renders correctly by opening it in the browser.

  • Make sure index.html is open in the editor (click on it in the Explorer sidebar if it's not).
  • Right-click anywhere inside the index.html editor and select Open with Live Server.

Your default browser opens and you should see unstyled text: the heading, a search input with a button, and labels like Open, High, Low, Prev Close, Market Cap, and Industry.

Don't see Open with Live Server?

Make sure you opened the stock-tracker folder (not just the file) in VS Code. Live Server needs a workspace folder to activate. Go to File then Open Folder and select the stock-tracker folder again.

Help me get Live Server working in VS Code.

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and that your browser is showing the raw page structure.

ⓧ I'd like to double check the full code

Here is the complete index.html file. Compare yours to this:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Stock Tracker</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>📈 Stock Tracker</h1>
            <p>Search any publicly traded company</p>
        </header>

        <div class="search-section">
            <form id="search-form">
                <input
                    type="text"
                    id="symbol-input"
                    placeholder="Enter ticker symbol (e.g., AAPL)"
                    autocomplete="off"
                    required
                >
                <button type="submit" id="search-btn">Search</button>
            </form>
        </div>

        <div id="loading" class="loading hidden">
            <div class="spinner"></div>
            <p>Fetching data...</p>
        </div>

        <div id="error-message" class="error hidden"></div>

        <div id="stock-card" class="stock-card hidden">
            <div class="company-header">
                <img id="company-logo" src="" alt="Company logo">
                <div class="company-info">
                    <h2 id="company-name"></h2>
                    <p id="company-details"></p>
                </div>
            </div>

            <div class="price-section">
                <div class="current-price">
                    <span id="stock-price"></span>
                    <span id="price-change"></span>
                </div>
            </div>

            <div class="stats-grid">
                <div class="stat">
                    <span class="stat-label">Open</span>
                    <span id="stat-open" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">High</span>
                    <span id="stat-high" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Low</span>
                    <span id="stat-low" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Prev Close</span>
                    <span id="stat-prev-close" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Market Cap</span>
                    <span id="stat-market-cap" class="stat-value"></span>
                </div>
                <div class="stat">
                    <span class="stat-label">Industry</span>
                    <span id="stat-industry" class="stat-value"></span>
                </div>
            </div>
        </div>
    </div>

    <script src="script.js"></script>
</body>
</html>

Your HTML skeleton is complete. Next up, you'll bring this page to life with CSS styling to create the sleek dark theme.

Style the App with CSS

Your HTML structure is in place, but right now it looks like plain text on a white page. A professional stock tracker needs a dark, polished interface that makes financial data easy to read at a glance.

In this step, you'll transform the raw HTML into a sleek dark-themed app using CSS Grid, flexbox layouts, and custom color-coded indicators for stock price changes.

In this step, get ready to:
  • Set up a dark theme with base styles and a styled search form.
  • Style the stock card with company info, price display, and stats grid.
  • Add loading animations, error states, and mobile responsiveness.
Set up the dark theme base styles

Every polished app starts with a reset and a base theme. The reset removes default browser spacing so your styles render consistently across Chrome, Safari, and Firefox.

  • In VS Code, open style.css from the Explorer sidebar.
  • Add the CSS reset, body, container, and header styles by pasting this code:
/* Reset default browser spacing */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

/* Dark theme base */
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: #0f0f1a;
    color: #e0e0e0;
    min-height: 100vh;
    padding: 2rem 1rem;
}

/* Center content with a max width */
.container {
    max-width: 600px;
    margin: 0 auto;
}

/* Header styling */
header {
    text-align: center;
    margin-bottom: 2rem;
}

header h1 {
    font-size: 2rem;
    color: #ffffff;
    margin-bottom: 0.5rem;
}

header p {
    color: #888;
    font-size: 1rem;
}

What does this code do?

The * selector resets margins and padding on all elements so you start from a clean slate.

The body sets up the dark navy background (#0f0f1a) and light text color that give the app its professional financial-dashboard feel. The .container limits the layout to 600px wide and centers it on the page.

  • Save style.css.
  • Switch to your browser. You should see the page now has a dark navy background with centered white text.

Still seeing a white background?

Make sure you saved style.css. Check that your index.html has <link rel="stylesheet" href="style.css"> in the <head> section. The filename must match exactly, including the lowercase spelling.

Help me debug why my CSS isn't loading.

Now let's style the search form. The input and button sit side-by-side using flexbox, with a blue accent color to draw the user's eye.

  • Add the search form styles below your header styles in style.css:
/* Search form layout */
.search-section {
    margin-bottom: 2rem;
}

#search-form {
    display: flex;
    gap: 0.5rem;
}

/* Dark input field with focus highlight */
#symbol-input {
    flex: 1;
    padding: 0.875rem 1rem;
    border: 1px solid #2a2a3e;
    border-radius: 8px;
    background-color: #1a1a2e;
    color: #ffffff;
    font-size: 1rem;
    outline: none;
    transition: border-color 0.2s;
}

#symbol-input:focus {
    border-color: #4a9eff;
}

#symbol-input::placeholder {
    color: #666;
}

/* Blue submit button */
#search-btn {
    padding: 0.875rem 1.5rem;
    background-color: #4a9eff;
    color: #ffffff;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}

#search-btn:hover {
    background-color: #3a8eef;
}

#search-btn:disabled {
    background-color: #333;
    cursor: not-allowed;
}

What does this code do?

The display: flex on the form places the input and button on the same row. The input takes up all remaining space with flex: 1.

The :focus pseudo-class makes the input border turn blue when the user clicks into it. The :disabled state will grey out the button later while data is loading.

  • Save style.css and check your browser.
  • You should see a dark input field with placeholder text and a blue Search button sitting side-by-side.

Input and button stacking vertically?

Check that your #search-form rule has display: flex. Without it, each element falls onto its own line. Also make sure the CSS selector matches your HTML exactly: id="search-form" in the HTML maps to #search-form in CSS.

Help me fix my flex layout.

Style the stock card and stats grid

The stock card is the main display area where company data will appear. It needs a clear visual hierarchy: company logo and name at the top, a prominent price in the middle, and supporting stats in a clean grid below.

  • Add the stock card container and company header styles below your search form styles:
/* Stock card container */
.stock-card {
    background-color: #1a1a2e;
    border: 1px solid #2a2a3e;
    border-radius: 12px;
    padding: 1.5rem;
}

/* Company header with logo and name side-by-side */
.company-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.5rem;
}

/* Company logo displayed on white background */
#company-logo {
    width: 48px;
    height: 48px;
    border-radius: 8px;
    object-fit: contain;
    background-color: #ffffff;
    padding: 4px;
}

.company-info h2 {
    font-size: 1.25rem;
    color: #ffffff;
}

.company-info p {
    color: #888;
    font-size: 0.875rem;
}

What does this code do?

The .stock-card gets a subtle border and rounded corners to create a card-like container. The .company-header uses flexbox to align the logo and company name horizontally.

The logo gets a white background because most company logos are designed for light backgrounds. The object-fit: contain ensures the logo scales proportionally without stretching.

  • Save style.css.
  • You won't see the stock card yet because it has the hidden class. You'll style that class shortly.
  • Now add the price display and stats grid styles below the company info styles:
/* Price display section with divider line */
.price-section {
    margin-bottom: 1.5rem;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid #2a2a3e;
}

.current-price {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
}

#stock-price {
    font-size: 2rem;
    font-weight: 700;
    color: #ffffff;
}

#price-change {
    font-size: 1rem;
    font-weight: 600;
}

/* Green for positive change, red for negative */
#price-change.positive {
    color: #4caf50;
}

#price-change.negative {
    color: #f44336;
}

/* Two-column stats grid */
.stats-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
}

.stat {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.stat-label {
    font-size: 0.75rem;
    color: #888;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.stat-value {
    font-size: 1rem;
    color: #e0e0e0;
    font-weight: 500;
}

What does this code do?

The price section separates itself from the stats with a bottom border. The .positive and .negative classes will be toggled by JavaScript later to color the price change green or red.

The .stats-grid uses CSS Grid to arrange the six stats (Open, High, Low, Prev Close, Market Cap, Industry) in two columns automatically.

  • Save style.css.
  • The stock card is still hidden in the browser. You'll see it once you wire up JavaScript in the next steps.
Add loading, error, and responsive styles

Users need visual feedback while the app fetches data. A spinning animation tells them something is happening. Error styling makes problems immediately obvious with red coloring.

  • Add the loading spinner and error styles below your stats grid styles:
/* Loading spinner animation */
.loading {
    text-align: center;
    padding: 2rem;
}

.spinner {
    width: 40px;
    height: 40px;
    border: 3px solid #2a2a3e;
    border-top-color: #4a9eff;
    border-radius: 50%;
    margin: 0 auto 1rem;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

/* Error message styling */
.error {
    background-color: #2d1b1b;
    border: 1px solid #5c2a2a;
    color: #ff6b6b;
    padding: 1rem;
    border-radius: 8px;
    text-align: center;
    margin-bottom: 1rem;
}

What does this code do?

The .spinner creates a circle where only the top border is blue. The @keyframes spin animation rotates it 360 degrees continuously, creating the spinning effect.

The .error class uses a dark red background with red text so error messages are impossible to miss.

  • Save style.css.
  • Both the loading and error elements are hidden right now. You'll see them in action once JavaScript toggles their visibility.
  • Finally, add the hidden utility class and the mobile responsive styles at the bottom of style.css:
/* Utility class to hide elements */
.hidden {
    display: none;
}

/* Mobile responsive adjustments */
@media (max-width: 480px) {
    #search-form {
        flex-direction: column;
    }

    .stats-grid {
        grid-template-columns: 1fr 1fr;
    }
}

What does this code do?

The .hidden class is a utility that JavaScript will add and remove to show or hide elements like the stock card, loading spinner, and error message.

The @media query kicks in on screens 480px or smaller. It stacks the search input and button vertically so everything still looks good on phones.

  • Save style.css and check your browser.
  • You should see a dark-themed page with a centered title, subtitle, and a styled search form (dark input field with a blue button). The stock card, loading spinner, and error areas are all hidden.

Page looks broken or partially styled?

Check for missing semicolons or unclosed curly braces in your CSS. A single missing } can break every rule that follows it. Open DevTools (F12) and look in the Console tab for CSS parsing errors.

Help me find the CSS syntax error.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file, then move on to the next step.

ⓧ I'd like to double check the full code

Here is the complete style.css file. Compare your code against it:

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background-color: #0f0f1a;
    color: #e0e0e0;
    min-height: 100vh;
    padding: 2rem 1rem;
}

.container {
    max-width: 600px;
    margin: 0 auto;
}

header {
    text-align: center;
    margin-bottom: 2rem;
}

header h1 {
    font-size: 2rem;
    color: #ffffff;
    margin-bottom: 0.5rem;
}

header p {
    color: #888;
    font-size: 1rem;
}

.search-section {
    margin-bottom: 2rem;
}

#search-form {
    display: flex;
    gap: 0.5rem;
}

#symbol-input {
    flex: 1;
    padding: 0.875rem 1rem;
    border: 1px solid #2a2a3e;
    border-radius: 8px;
    background-color: #1a1a2e;
    color: #ffffff;
    font-size: 1rem;
    outline: none;
    transition: border-color 0.2s;
}

#symbol-input:focus {
    border-color: #4a9eff;
}

#symbol-input::placeholder {
    color: #666;
}

#search-btn {
    padding: 0.875rem 1.5rem;
    background-color: #4a9eff;
    color: #ffffff;
    border: none;
    border-radius: 8px;
    font-size: 1rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.2s;
}

#search-btn:hover {
    background-color: #3a8eef;
}

#search-btn:disabled {
    background-color: #333;
    cursor: not-allowed;
}

.loading {
    text-align: center;
    padding: 2rem;
}

.spinner {
    width: 40px;
    height: 40px;
    border: 3px solid #2a2a3e;
    border-top-color: #4a9eff;
    border-radius: 50%;
    margin: 0 auto 1rem;
    animation: spin 0.8s linear infinite;
}

@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}

.error {
    background-color: #2d1b1b;
    border: 1px solid #5c2a2a;
    color: #ff6b6b;
    padding: 1rem;
    border-radius: 8px;
    text-align: center;
    margin-bottom: 1rem;
}

.stock-card {
    background-color: #1a1a2e;
    border: 1px solid #2a2a3e;
    border-radius: 12px;
    padding: 1.5rem;
}

.company-header {
    display: flex;
    align-items: center;
    gap: 1rem;
    margin-bottom: 1.5rem;
}

#company-logo {
    width: 48px;
    height: 48px;
    border-radius: 8px;
    object-fit: contain;
    background-color: #ffffff;
    padding: 4px;
}

.company-info h2 {
    font-size: 1.25rem;
    color: #ffffff;
}

.company-info p {
    color: #888;
    font-size: 0.875rem;
}

.price-section {
    margin-bottom: 1.5rem;
    padding-bottom: 1.5rem;
    border-bottom: 1px solid #2a2a3e;
}

.current-price {
    display: flex;
    align-items: baseline;
    gap: 0.75rem;
}

#stock-price {
    font-size: 2rem;
    font-weight: 700;
    color: #ffffff;
}

#price-change {
    font-size: 1rem;
    font-weight: 600;
}

#price-change.positive {
    color: #4caf50;
}

#price-change.negative {
    color: #f44336;
}

.stats-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1rem;
}

.stat {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.stat-label {
    font-size: 0.75rem;
    color: #888;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.stat-value {
    font-size: 1rem;
    color: #e0e0e0;
    font-weight: 500;
}

.hidden {
    display: none;
}

@media (max-width: 480px) {
    #search-form {
        flex-direction: column;
    }

    .stats-grid {
        grid-template-columns: 1fr 1fr;
    }
}

Your app looks professional and polished. Next up, you'll bring it to life by connecting to the Finnhub API with JavaScript to fetch real stock data.

Connect to the Finnhub API

Your HTML structure and styling are complete. The app looks great, but it does nothing when you click Search. Now it is time to bring it to life with real financial data.

In this step, you will write the JavaScript that connects your search form to the Finnhub API. You will use async/await to fetch data from two endpoints simultaneously, then display the results in your stock card.

In this step, get ready to:
  • Set up API constants and grab references to your HTML elements.
  • Write async functions to fetch stock quotes and company profiles from Finnhub.
  • Display fetched data in the stock card with formatted prices and stats.
Set up your API constants and DOM references

Before you can fetch data, your JavaScript needs two things: the API connection details and references to the HTML elements it will update.

  • In VS Code, open script.js from the Explorer sidebar.
  • Add the following code to set up your API constants, DOM references, and event listener:
const API_KEY = 'your-api-key-here';
const BASE_URL = 'https://finnhub.io/api/v1';

// Grab references to the HTML elements we need to interact with
const searchForm = document.getElementById('search-form');
const symbolInput = document.getElementById('symbol-input');
const searchBtn = document.getElementById('search-btn');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error-message');
const stockCard = document.getElementById('stock-card');

// Listen for form submissions and trigger a search
searchForm.addEventListener('submit', handleSearch);

What does this code do?

API_KEY stores your Finnhub API key so every request can authenticate. BASE_URL holds the root URL all API requests will build from.

The six DOM references let your JavaScript find and update HTML elements without searching the document every time. The event listener watches for form submissions and calls handleSearch (which you will write next).

  • Replace your-api-key-here with the Finnhub API key you copied in Step 1.
  • Save script.js.

Not sure where your API key is?

Return to finnhub.io and sign in. Your API key is shown on your dashboard page. Copy the full key string and paste it between the single quotes.

Help me find my Finnhub API key.

Write the search handler function

When the learner submits the form, you need a function that reads the ticker symbol, fetches data from both Finnhub endpoints at the same time, and passes the results to a display function.

  • In script.js, add the handleSearch function below the event listener:
async function handleSearch(event) {
    // Stop the form from refreshing the page
    event.preventDefault();
    const symbol = symbolInput.value.trim().toUpperCase();
    if (!symbol) return;

    // Fetch both the quote and company profile at the same time
    const [quote, profile] = await Promise.all([
        fetchQuote(symbol),
        fetchProfile(symbol)
    ]);

    displayStockData(quote, profile);
}

What does this code do?

event.preventDefault() stops the browser from refreshing the page when the form submits. Without this, your app would reload and lose everything.

Promise.all fires both API requests simultaneously instead of waiting for one to finish before starting the other. This makes the app faster because both network calls happen in parallel.

What is Promise.all?

Promise.all takes an array of Promises and waits for all of them to finish. The result is an array of responses in the same order you passed them in.

If either request fails, the whole Promise.all fails. You will add proper error handling in the next step.

  • Save script.js. You will not see a change yet because fetchQuote and fetchProfile do not exist yet.
Write the API fetch functions

Each fetch function builds a URL for one Finnhub endpoint, makes the network request, and returns the parsed JSON response.

  • Add the fetchQuote function below handleSearch:
async function fetchQuote(symbol) {
    // Build the quote endpoint URL with the symbol and API key
    const url = `${BASE_URL}/quote?symbol=${symbol}&token=${API_KEY}`;
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Failed to fetch stock quote. Please try again.');
    }

    return response.json();
}

What does this code do?

The Fetch API sends an HTTP request to the Finnhub quote endpoint. The await keyword pauses execution until the network response arrives.

The response.ok check catches HTTP errors (like 401 Unauthorized or 500 Server Error). If something goes wrong, the function throws an error instead of returning bad data.

  • Add the fetchProfile function directly below fetchQuote:
async function fetchProfile(symbol) {
    // Build the company profile endpoint URL
    const url = `${BASE_URL}/stock/profile2?symbol=${symbol}&token=${API_KEY}`;
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Failed to fetch company profile. Please try again.');
    }

    return response.json();
}

What does this code do?

This follows the exact same pattern as fetchQuote, but hits the /stock/profile2 endpoint instead. That endpoint returns company details like name, logo, industry, exchange, and market cap.

  • Save script.js. The app still will not display data because displayStockData has not been written yet.

Seeing a console error about handleSearch?

If your browser console shows an error about handleSearch not being defined, make sure the event listener line sits above the function. JavaScript hoists function declarations, so this should work. Check for typos in the function name.

Why is my event listener throwing an error?

Display the stock data in the UI

Now you need a function that takes the raw API data and populates your stock card with formatted values.

  • Add the displayStockData function below fetchProfile:
function displayStockData(quote, profile) {
    // Get references to all the elements we need to fill in
    const companyLogo = document.getElementById('company-logo');
    const companyName = document.getElementById('company-name');
    const companyDetails = document.getElementById('company-details');
    const stockPrice = document.getElementById('stock-price');
    const priceChange = document.getElementById('price-change');
    const statOpen = document.getElementById('stat-open');
    const statHigh = document.getElementById('stat-high');
    const statLow = document.getElementById('stat-low');
    const statPrevClose = document.getElementById('stat-prev-close');
    const statMarketCap = document.getElementById('stat-market-cap');
    const statIndustry = document.getElementById('stat-industry');

    // Set the company logo or show a fallback
    if (profile.logo) {
        companyLogo.src = profile.logo;
        companyLogo.alt = `${profile.name} logo`;
    } else {
        companyLogo.src = '';
        companyLogo.alt = 'No logo available';
    }

    // Fill in company name and details
    companyName.textContent = profile.name || 'Unknown Company';
    companyDetails.textContent = `${profile.ticker || ''} \u2022 ${profile.exchange || ''} \u2022 ${profile.country || ''}`;

    // Display the current stock price
    stockPrice.textContent = `$${quote.c.toFixed(2)}`;

    // Calculate change and determine if positive or negative
    const change = quote.d !== null && quote.d !== undefined ? quote.d : (quote.c - quote.pc);
    const changePercent = quote.dp !== null && quote.dp !== undefined ? quote.dp : ((quote.c - quote.pc) / quote.pc * 100);
    const isPositive = change >= 0;

    // Show the change with a + or - prefix and color it green/red
    priceChange.textContent = `${isPositive ? '+' : ''}${change.toFixed(2)} (${isPositive ? '+' : ''}${changePercent.toFixed(2)}%)`;
    priceChange.className = isPositive ? 'positive' : 'negative';

    // Fill in the stats grid
    statOpen.textContent = `$${quote.o.toFixed(2)}`;
    statHigh.textContent = `$${quote.h.toFixed(2)}`;
    statLow.textContent = `$${quote.l.toFixed(2)}`;
    statPrevClose.textContent = `$${quote.pc.toFixed(2)}`;
    statMarketCap.textContent = formatMarketCap(profile.marketCapitalization);
    statIndustry.textContent = profile.finnhubIndustry || 'N/A';

    // Reveal the stock card
    stockCard.classList.remove('hidden');
}

What does this code do?

This function takes the two API responses and maps their fields onto your HTML elements. Finnhub returns values like quote.c (current price), quote.d (dollar change), and quote.dp (percent change).

The isPositive check determines whether the price went up or down. It sets the CSS class to positive (green) or negative (red) so your existing styles color-code the change.

  • Add the formatMarketCap helper function below displayStockData:
function formatMarketCap(value) {
    // Convert raw millions into a readable format (T/B/M)
    if (!value) return 'N/A';
    if (value >= 1000000) {
        return `$${(value / 1000000).toFixed(2)}T`;
    }
    if (value >= 1000) {
        return `$${(value / 1000).toFixed(2)}B`;
    }
    return `$${value.toFixed(2)}M`;
}

What does this code do?

Finnhub returns market capitalization in millions. A company worth $3.5 trillion comes back as 3500000. This helper divides the value down and appends T (trillions), B (billions), or M (millions) so users see a readable number.

  • Save script.js.
  • Switch to your browser where Live Server is running.
  • Type AAPL into the search field and click Search.
  • You should see the stock card appear with Apple's logo, name, current price (in green or red), and a stats grid showing open, high, low, previous close, market cap, and industry.

Stock card not appearing?

Open your browser's DevTools with F12 and check the Console tab for errors.

  • If you see a 401 or 403 error, your API key is incorrect. Double-check you pasted the full key between the quotes.
  • If you see fetchQuote is not defined, make sure all your functions are in the same file with no typos in the names.
  • If the card appears but shows $0.00 for everything, you may have typed a ticker that does not exist. Try a common one like MSFT or TSLA.

Help me debug why my stock card is not showing data.

✔️ Awesome, I've got everything!

Great. Make sure you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete script.js at this point. Compare yours against it:

const API_KEY = 'your-api-key-here';
const BASE_URL = 'https://finnhub.io/api/v1';

const searchForm = document.getElementById('search-form');
const symbolInput = document.getElementById('symbol-input');
const searchBtn = document.getElementById('search-btn');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error-message');
const stockCard = document.getElementById('stock-card');

searchForm.addEventListener('submit', handleSearch);

async function handleSearch(event) {
    event.preventDefault();
    const symbol = symbolInput.value.trim().toUpperCase();
    if (!symbol) return;

    const [quote, profile] = await Promise.all([
        fetchQuote(symbol),
        fetchProfile(symbol)
    ]);

    displayStockData(quote, profile);
}

async function fetchQuote(symbol) {
    const url = `${BASE_URL}/quote?symbol=${symbol}&token=${API_KEY}`;
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Failed to fetch stock quote. Please try again.');
    }

    return response.json();
}

async function fetchProfile(symbol) {
    const url = `${BASE_URL}/stock/profile2?symbol=${symbol}&token=${API_KEY}`;
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Failed to fetch company profile. Please try again.');
    }

    return response.json();
}

function displayStockData(quote, profile) {
    const companyLogo = document.getElementById('company-logo');
    const companyName = document.getElementById('company-name');
    const companyDetails = document.getElementById('company-details');
    const stockPrice = document.getElementById('stock-price');
    const priceChange = document.getElementById('price-change');
    const statOpen = document.getElementById('stat-open');
    const statHigh = document.getElementById('stat-high');
    const statLow = document.getElementById('stat-low');
    const statPrevClose = document.getElementById('stat-prev-close');
    const statMarketCap = document.getElementById('stat-market-cap');
    const statIndustry = document.getElementById('stat-industry');

    if (profile.logo) {
        companyLogo.src = profile.logo;
        companyLogo.alt = `${profile.name} logo`;
    } else {
        companyLogo.src = '';
        companyLogo.alt = 'No logo available';
    }

    companyName.textContent = profile.name || 'Unknown Company';
    companyDetails.textContent = `${profile.ticker || ''} \u2022 ${profile.exchange || ''} \u2022 ${profile.country || ''}`;

    stockPrice.textContent = `$${quote.c.toFixed(2)}`;

    const change = quote.d !== null && quote.d !== undefined ? quote.d : (quote.c - quote.pc);
    const changePercent = quote.dp !== null && quote.dp !== undefined ? quote.dp : ((quote.c - quote.pc) / quote.pc * 100);
    const isPositive = change >= 0;

    priceChange.textContent = `${isPositive ? '+' : ''}${change.toFixed(2)} (${isPositive ? '+' : ''}${changePercent.toFixed(2)}%)`;
    priceChange.className = isPositive ? 'positive' : 'negative';

    statOpen.textContent = `$${quote.o.toFixed(2)}`;
    statHigh.textContent = `$${quote.h.toFixed(2)}`;
    statLow.textContent = `$${quote.l.toFixed(2)}`;
    statPrevClose.textContent = `$${quote.pc.toFixed(2)}`;
    statMarketCap.textContent = formatMarketCap(profile.marketCapitalization);
    statIndustry.textContent = profile.finnhubIndustry || 'N/A';

    stockCard.classList.remove('hidden');
}

function formatMarketCap(value) {
    if (!value) return 'N/A';
    if (value >= 1000000) {
        return `$${(value / 1000000).toFixed(2)}T`;
    }
    if (value >= 1000) {
        return `$${(value / 1000).toFixed(2)}B`;
    }
    return `$${value.toFixed(2)}M`;
}

Your stock tracker is pulling live data from the internet. Right now it works perfectly for valid tickers, but what happens when someone searches for a ticker that does not exist? Next, you will add loading spinners and error handling to make the app feel polished and professional.

Add Loading and Error States

Your app can now fetch and display stock data. But what happens while the data is loading? Or when a user types a ticker symbol that does not exist?

Professional apps communicate clearly with users during every state: loading, success, and error. In this step, you'll add helper functions that manage these UI states and wrap your API calls in error handling so the app never silently breaks.

In this step, get ready to:
  • Write helper functions that toggle loading, error, and card visibility.
  • Add try/catch/finally error handling to your search function.
  • Test with a valid ticker and an invalid one to confirm both paths work.
Write the UI state helper functions

Each helper toggles the .hidden CSS class on the relevant DOM element. Keeping these as small, named functions makes the main search logic easier to read.

  • In script.js, scroll to the bottom of the file (after the formatMarketCap function) and add these helper functions:
// Show the loading spinner and disable the search button
function showLoading() {
    loadingEl.classList.remove('hidden');
    searchBtn.disabled = true;
}

// Hide the loading spinner and re-enable the search button
function hideLoading() {
    loadingEl.classList.add('hidden');
    searchBtn.disabled = false;
}

// Display an error message to the user
function showError(message) {
    errorEl.textContent = message;
    errorEl.classList.remove('hidden');
}

// Hide any visible error message
function hideError() {
    errorEl.classList.add('hidden');
}

// Hide the stock card from view
function hideStockCard() {
    stockCard.classList.add('hidden');
}

What does this code do?

Each function controls one piece of UI state by adding or removing the hidden class you defined in your CSS.

  • showLoading reveals the spinner and disables the button so the user cannot spam searches.
  • hideLoading reverses that: hides the spinner and re-enables the button.
  • showError sets the error text and makes the error div visible.
  • hideError and hideStockCard hide their respective elements so old results clear before a new search.
  • Save script.js.

You will not see a change yet. The next chunk wires these helpers into the search flow.

Add error handling to the search function

Right now, if a fetch fails or the user types a bogus ticker, the app crashes silently. Wrapping the API calls in a try/catch/finally block lets you catch errors, show a message, and always clean up the loading state.

  • In script.js, find the existing handleSearch function. It currently looks like this:
async function handleSearch(event) {
    event.preventDefault();
    const symbol = symbolInput.value.trim().toUpperCase();
    if (!symbol) return;

    const [quote, profile] = await Promise.all([
        fetchQuote(symbol),
        fetchProfile(symbol)
    ]);

    displayStockData(quote, profile);
}
  • Replace the entire handleSearch function with this updated version:
async function handleSearch(event) {
    event.preventDefault();
    const symbol = symbolInput.value.trim().toUpperCase();
    if (!symbol) return;

    // Reset the UI before starting a new search
    showLoading();
    hideError();
    hideStockCard();

    try {
        // Fetch both quote and profile data at the same time
        const [quote, profile] = await Promise.all([
            fetchQuote(symbol),
            fetchProfile(symbol)
        ]);

        // Check if the API returned empty data (invalid symbol)
        if (quote.c === 0 && quote.h === 0) {
            throw new Error('No data found for this symbol. Please check the ticker and try again.');
        }

        displayStockData(quote, profile);
    } catch (error) {
        showError(error.message);
    } finally {
        // Always hide loading, whether the request succeeded or failed
        hideLoading();
    }
}

What does this code do?

  • Before the fetch starts, three helper calls reset the UI: show the spinner, hide old errors, and hide old results.
  • try wraps the risky API calls. If anything throws an error, execution jumps straight to catch.
  • The quote.c === 0 && quote.h === 0 check catches invalid tickers. Finnhub returns zeros instead of an HTTP error for unknown symbols.
  • finally always runs, whether the request succeeded or failed. This guarantees the spinner is hidden and the button is re-enabled.
  • Save script.js.
  • Switch to your browser. The page should auto-refresh via Live Server.

Seeing a console error on page load?

  • Make sure you deleted the entire old handleSearch function and replaced it fully. A leftover brace or duplicate function name causes a syntax error.
  • Check that the five helper functions (showLoading, hideLoading, showError, hideError, hideStockCard) exist below formatMarketCap. If handleSearch calls them before they are defined, you will get a "not a function" error.
  • help me debug a console error in my stock tracker
Test the loading and error states

Time to confirm both the happy path and the error path work correctly.

  • In the browser, type AAPL into the search field and click Search.
  • Watch for the loading spinner. It should appear briefly, then the stock card should display Apple's data.
  • Now clear the input, type XYZFAKE123, and click Search.
  • You should see a red error box with the message "No data found for this symbol. Please check the ticker and try again."
  • Search for AAPL again. The error should disappear and the stock card should return.

Why test both paths?

Verifying both success and failure confirms your try/catch/finally logic is wired correctly. The error-then-success test also proves that hideError properly clears previous errors before each new search.

Not seeing the error message for the fake ticker?

  • Open DevTools (press F12) and check the Console tab for errors. An uncaught error means your catch block might be missing or misplaced.
  • Make sure you have the if (quote.c === 0 && quote.h === 0) check inside the try block, after the Promise.all line. This is what detects invalid tickers.
  • Confirm your API key is still valid by checking the Network tab in DevTools. If the response status is 401, your key may have expired.
  • help me troubleshoot my stock tracker error handling

✔️ Awesome, I've got everything!

Great. Double check you have saved script.js and that both the AAPL and XYZFAKE123 tests pass.

ⓧ I'd like to double check the full code

Here is the complete script.js at this point. Compare yours against it:

const API_KEY = 'your-api-key-here';
const BASE_URL = 'https://finnhub.io/api/v1';

const searchForm = document.getElementById('search-form');
const symbolInput = document.getElementById('symbol-input');
const searchBtn = document.getElementById('search-btn');
const loadingEl = document.getElementById('loading');
const errorEl = document.getElementById('error-message');
const stockCard = document.getElementById('stock-card');

searchForm.addEventListener('submit', handleSearch);

async function handleSearch(event) {
    event.preventDefault();
    const symbol = symbolInput.value.trim().toUpperCase();
    if (!symbol) return;

    showLoading();
    hideError();
    hideStockCard();

    try {
        const [quote, profile] = await Promise.all([
            fetchQuote(symbol),
            fetchProfile(symbol)
        ]);

        if (quote.c === 0 && quote.h === 0) {
            throw new Error('No data found for this symbol. Please check the ticker and try again.');
        }

        displayStockData(quote, profile);
    } catch (error) {
        showError(error.message);
    } finally {
        hideLoading();
    }
}

async function fetchQuote(symbol) {
    const url = `${BASE_URL}/quote?symbol=${symbol}&token=${API_KEY}`;
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Failed to fetch stock quote. Please try again.');
    }

    return response.json();
}

async function fetchProfile(symbol) {
    const url = `${BASE_URL}/stock/profile2?symbol=${symbol}&token=${API_KEY}`;
    const response = await fetch(url);

    if (!response.ok) {
        throw new Error('Failed to fetch company profile. Please try again.');
    }

    return response.json();
}

function displayStockData(quote, profile) {
    const companyLogo = document.getElementById('company-logo');
    const companyName = document.getElementById('company-name');
    const companyDetails = document.getElementById('company-details');
    const stockPrice = document.getElementById('stock-price');
    const priceChange = document.getElementById('price-change');
    const statOpen = document.getElementById('stat-open');
    const statHigh = document.getElementById('stat-high');
    const statLow = document.getElementById('stat-low');
    const statPrevClose = document.getElementById('stat-prev-close');
    const statMarketCap = document.getElementById('stat-market-cap');
    const statIndustry = document.getElementById('stat-industry');

    if (profile.logo) {
        companyLogo.src = profile.logo;
        companyLogo.alt = `${profile.name} logo`;
    } else {
        companyLogo.src = '';
        companyLogo.alt = 'No logo available';
    }

    companyName.textContent = profile.name || 'Unknown Company';
    companyDetails.textContent = `${profile.ticker || ''} \u2022 ${profile.exchange || ''} \u2022 ${profile.country || ''}`;

    stockPrice.textContent = `$${quote.c.toFixed(2)}`;

    const change = quote.d !== null && quote.d !== undefined ? quote.d : (quote.c - quote.pc);
    const changePercent = quote.dp !== null && quote.dp !== undefined ? quote.dp : ((quote.c - quote.pc) / quote.pc * 100);
    const isPositive = change >= 0;

    priceChange.textContent = `${isPositive ? '+' : ''}${change.toFixed(2)} (${isPositive ? '+' : ''}${changePercent.toFixed(2)}%)`;
    priceChange.className = isPositive ? 'positive' : 'negative';

    statOpen.textContent = `$${quote.o.toFixed(2)}`;
    statHigh.textContent = `$${quote.h.toFixed(2)}`;
    statLow.textContent = `$${quote.l.toFixed(2)}`;
    statPrevClose.textContent = `$${quote.pc.toFixed(2)}`;
    statMarketCap.textContent = formatMarketCap(profile.marketCapitalization);
    statIndustry.textContent = profile.finnhubIndustry || 'N/A';

    stockCard.classList.remove('hidden');
}

function formatMarketCap(value) {
    if (!value) return 'N/A';
    if (value >= 1000000) {
        return `$${(value / 1000000).toFixed(2)}T`;
    }
    if (value >= 1000) {
        return `$${(value / 1000).toFixed(2)}B`;
    }
    return `$${value.toFixed(2)}M`;
}

function showLoading() {
    loadingEl.classList.remove('hidden');
    searchBtn.disabled = true;
}

function hideLoading() {
    loadingEl.classList.add('hidden');
    searchBtn.disabled = false;
}

function showError(message) {
    errorEl.textContent = message;
    errorEl.classList.remove('hidden');
}

function hideError() {
    errorEl.classList.add('hidden');
}

function hideStockCard() {
    stockCard.classList.add('hidden');
}

Your stock tracker now handles every user scenario gracefully: loading feedback while data is in transit, clear error messages for bad input, and smooth transitions between states. Next up, you will deploy this app live on the internet so anyone can use it.

Deploy to Vercel via GitHub

Your stock tracker works perfectly on your machine. Now it is time to put it on the internet so anyone can use it.

In this step, you will push your code to GitHub and then deploy it live using Vercel. Vercel connects directly to your GitHub repository and automatically publishes your site every time you push new code.

In this step, get ready to:
  • Initialize a Git repository and make your first commit.
  • Push your project to a GitHub repository.
  • Deploy your app live on Vercel.
Initialize Git and make your first commit

Version control tracks every change you make to your code. Before you can share your project on GitHub, you need to create a Git repository in your project folder.

  • In VS Code, open the Source Control panel by pressing Ctrl+Shift+G (Windows) or Cmd+Shift+G (macOS).
  • Click the Initialize Repository button.

What does Initialize Repository do?

This creates a hidden .git folder in your project that tracks every change you make. Your files now appear in the Source Control panel as untracked changes, ready to be committed.

  • Create a .gitignore file by opening the integrated terminal (Ctrl+` on Windows or Cmd+` on macOS) and running this command:
touch .gitignore

Why create an empty .gitignore?

A .gitignore file tells Git which files to exclude from version control. For this project it can stay empty, but having one in place is a best practice. If you later add sensitive files like .env, you can list them here to keep them out of your repository.

  • In the Source Control panel, hover over Changes and click the + (Stage All Changes) button to stage all your files.
  • Type Initial commit: stock tracker app into the message field at the top of the Source Control panel.
  • Click the Commit button (the checkmark icon) to save your commit.
  • Confirm the files exist in your commit by running:
git log --oneline

You should see a single commit with the message Initial commit: stock tracker app.

Don't see the commit?

Make sure you staged your files before committing. If the Source Control panel still shows changes, click the + button next to Changes, enter your message, and commit again.

Help me fix my Git commit in VS Code

Push your code to GitHub

Your commit lives only on your machine. Pushing it to GitHub gives you a backup and creates the connection that Vercel needs for deployment.

  • Open your browser and go to github.com/new.
  • Enter stock-tracker as the repository name.
  • Keep the repository set to Public.
  • Leave all other options as their defaults (no README, no .gitignore, no license).
  • Click Create repository.

GitHub shows a page with setup instructions. You need the HTTPS URL at the top (it looks like https://github.com/your-username/stock-tracker.git).

  • Copy the repository URL from GitHub.
  • Switch back to VS Code's terminal and connect your local repo to GitHub by running this command:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/stock-tracker.git"]]
  • Push your code to GitHub by running:
git push -u origin main

What does this command do?

The -u flag sets origin main as the default remote branch. After this, you can just type git push for future updates without specifying the remote and branch every time.

GitHub authentication prompt

VS Code may open a browser window asking you to sign in to GitHub. Follow the prompts to authorize VS Code. Once authenticated, the push will complete automatically.

  • Go back to your browser and refresh the GitHub repository page.

You should see your four files (index.html, style.css, script.js, .gitignore) listed in the repository.

Push failed or rejected?

If you see an error about the remote URL, double-check that you copied the full HTTPS URL from GitHub. You can verify it by running git remote -v in your terminal.

If Git asks for a username and password, use your GitHub username and a Personal Access Token (not your GitHub password). GitHub no longer accepts passwords for terminal authentication.

Help me fix my git push to GitHub

Deploy your app on Vercel

Your code is on GitHub. Now you will connect it to Vercel, a hosting platform that deploys static sites in seconds.

  • Open your browser and go to vercel.com.
  • Click Sign Up.
  • Select GitHub as your sign-up method.
  • Authorize Vercel to access your GitHub account when prompted.

Why sign up with GitHub?

Signing up with GitHub lets Vercel automatically see your repositories. You will not need to manually connect the two services later.

  • Once signed in, you will be taken to the Vercel dashboard. Click Add New... and select Project.
  • Find your stock-tracker repository in the list and click Import next to it.
  • On the configuration page, set the Framework Preset dropdown to Other.
  • Leave all other settings as their defaults.
  • Click Deploy.

Why set Framework Preset to Other?

Your project is plain HTML, CSS, and JavaScript with no build step. Setting the preset to Other tells Vercel to serve your files as-is from the root directory, without trying to run a build command.

Vercel will build and deploy your app in about 30 seconds. When it finishes, you will see a congratulations page with a preview of your live site.

  • Click the preview image or the generated URL (it looks like stock-tracker-xxx.vercel.app) to open your live app.
  • Test it by searching for AAPL to confirm stock data loads on the live site.

Seeing a 404 page on your Vercel URL?

File names are case-sensitive on Vercel. Make sure your file is named index.html (all lowercase). If it is Index.html or INDEX.html, Vercel cannot serve it at the root URL.

Also check that index.html is in the root of your repository, not inside a subfolder.

Help me fix my Vercel deployment

Secret mission

Build a Watchlist Dashboard

Your stock tracker works great for one-off searches. But what if you could save your favorite stocks and see them all at a glance? In this secret mission, you'll add a persistent watchlist that uses localStorage to remember your picks and fetches fresh data for all of them simultaneously with Promise.all.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project uses only free-tier services and local files, so there are no ongoing costs to worry about.

Resources you used:

  • Local stock-tracker project folder on your Desktop.
  • GitHub stock-tracker repository.
  • Vercel stock-tracker project and live deployment URL.
  • Finnhub API account (free tier, no charges).
  • Git for Windows.
  • Live Server VS Code extension.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep your live Vercel URL available to share.

  • Your Vercel deployment stays live at your project URL for free.
  • Your GitHub repository remains accessible and public.
  • Your Finnhub free-tier account stays active with 60 API calls per minute at no cost.
  • Git for Windows and Live Server remain installed and available for future projects.

Pause - I'll come back to this later

Since all services are free-tier, there is nothing to pause. Your Vercel deployment and GitHub repo stay live at no cost.

  • Close the VS Code window to free up memory.
  • Stop Live Server if it is still running by clicking Port: 5500 in the VS Code status bar.
  • Your project files, GitHub repo, Vercel deployment, and Finnhub account will all be waiting when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Delete the local project folder.
  • On macOS/Linux, open your terminal and run:
rm -rf ~/Desktop/stock-tracker
  • On Windows, open your terminal and run:
rmdir /s /q %USERPROFILE%\Desktop\stock-tracker
  • Delete the Vercel project. Go to your Vercel dashboard, open the stock-tracker project, click Settings, scroll to the bottom of the page, and click Delete.
  • Delete the GitHub repository. Go to your stock-tracker repository on GitHub, click Settings, scroll to the Danger Zone section, and click Delete this repository.
  • Uninstall the Live Server extension. In VS Code, open the Extensions panel (Cmd+Shift+X on macOS or Ctrl+Shift+X on Windows), find Live Server, and click Uninstall.
  • Uninstall Git for Windows. Open Settings, select Apps, search for Git, and click Uninstall.

Nice Work!

Nice Work!

You just built and deployed a real-world web application that pulls live financial data from an external API. Your Company Stock Tracker is live on the internet, ready to show anyone what you can build.

You've learned how to:

  • Built a responsive stock tracker UI with a dark theme, CSS Grid stats layout, and loading/error state management.
  • Connected to the Finnhub API using fetch, async/await, and Promise.all to retrieve company profiles and stock quotes simultaneously.
  • Deployed your app live on Vercel via GitHub with automatic redeployment on every push.
  • Secret Mission: Added a persistent watchlist dashboard using localStorage and batch API calls with Promise.all.

Ready to quiz yourself?