From Zero to AI

Lesson 6.1: API Keys

Duration: 45 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  1. Understand what API keys are and why they exist
  2. Know where to find and how to obtain API keys
  3. Send API keys in requests using different methods
  4. Store API keys securely and avoid common mistakes
  5. Handle API key errors properly

Introduction

Most APIs you will work with require some form of authentication. The simplest and most common method is the API key - a unique string that identifies your application to the API provider.

Think of an API key like a library card. Anyone can walk into a public library, but to borrow books, you need a card that identifies you. The library tracks who borrows what, and can revoke access if you break the rules. API keys work the same way.

// Without API key - often blocked or limited
const response = await fetch("https://api.weather.com/current");
// Error: 401 Unauthorized

// With API key - access granted
const response = await fetch("https://api.weather.com/current?apikey=abc123xyz");
// Success: weather data returned

What is an API Key?

An API key is a unique identifier - typically a long string of letters and numbers - that:

  1. Identifies your application to the API
  2. Authorizes access to the API's resources
  3. Tracks your usage (requests, data consumed)
  4. Enforces rate limits and quotas

Example API Keys

# OpenWeatherMap
appid=1a2b3c4d5e6f7g8h9i0j

# Google Maps
key=AIzaSyD-9tSrke72PouQMnMX-a7eZSW0jkFMBWY

# Stripe (test key)
sk_test_4eC39HqLyjWDarjtT1zdp7dc

# GitHub Personal Access Token
ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Notice how different providers use different formats, but they are all just strings that prove your identity.


Why Do APIs Require Keys?

1. Identification

The API provider needs to know who is making requests. This helps with:

  • Support and debugging
  • Billing (for paid APIs)
  • Communication about changes

2. Rate Limiting

APIs limit how many requests you can make per minute/hour/day. Keys track your usage:

Your API Key: abc123
- Requests today: 847 / 1000
- Requests this minute: 12 / 60

3. Security

Keys can be revoked if compromised, without affecting other users:

// If your key leaks, the provider can disable just your key
// Everyone else's applications continue working

4. Access Control

Different keys can have different permissions:

// Read-only key - can only GET data
const readKey = 'ro_abc123';

// Full access key - can GET, POST, PUT, DELETE
const adminKey = 'admin_xyz789';

Getting an API Key

The process varies by provider, but generally follows these steps:

Step 1: Create an Account

Most APIs require you to sign up on their developer portal:

Step 2: Create an Application/Project

Some providers ask you to register your application:

Application Name: My Weather App
Description: Personal weather dashboard
Website: http://localhost:3000

Step 3: Generate API Key

The portal will generate a unique key for you. Save it immediately - many providers only show it once!

Step 4: Note Any Restrictions

Read the documentation for:

  • Rate limits (requests per minute/day)
  • Allowed endpoints
  • Required attribution
  • Cost (if any)

Sending API Keys

There are three common ways to send an API key with your request:

Method 1: Query Parameter

The key is added to the URL:

const API_KEY = 'your-api-key';
const city = 'London';

const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${API_KEY}`;

const response = await fetch(url);
const weather = await response.json();

Pros: Simple, easy to test in browser Cons: Key visible in URL, browser history, server logs

Method 2: Request Header

The key is sent in a custom header:

const API_KEY = 'your-api-key';

const response = await fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': API_KEY,
  },
});

Common header names:

  • X-API-Key
  • X-Api-Key
  • Api-Key
  • Authorization

Pros: Not visible in URL, more secure Cons: Requires reading documentation for correct header name

Method 3: Authorization Header

A specific header format, often used with the word "Bearer" or "Basic":

const API_KEY = 'your-api-key';

// Simple API key
const response = await fetch('https://api.example.com/data', {
  headers: {
    Authorization: `ApiKey ${API_KEY}`,
  },
});

// Or some APIs use this format
const response2 = await fetch('https://api.example.com/data', {
  headers: {
    Authorization: API_KEY,
  },
});

Pros: Standard location, tools expect it Cons: Format varies between APIs


Practical Example: Weather API

Let us build a simple weather fetcher using the OpenWeatherMap API.

Setting Up

First, you would get a free API key from OpenWeatherMap.

The Code

interface WeatherResponse {
  name: string;
  main: {
    temp: number;
    humidity: number;
    feels_like: number;
  };
  weather: Array<{
    main: string;
    description: string;
  }>;
}

interface WeatherError {
  cod: string;
  message: string;
}

async function getWeather(city: string, apiKey: string): Promise<WeatherResponse> {
  const url = new URL('https://api.openweathermap.org/data/2.5/weather');
  url.searchParams.set('q', city);
  url.searchParams.set('appid', apiKey);
  url.searchParams.set('units', 'metric'); // Celsius

  const response = await fetch(url.toString());

  if (!response.ok) {
    const error: WeatherError = await response.json();
    throw new Error(`Weather API error: ${error.message}`);
  }

  return response.json();
}

// Usage
async function main() {
  const API_KEY = 'your-api-key-here';

  try {
    const weather = await getWeather('London', API_KEY);

    console.log(`Weather in ${weather.name}:`);
    console.log(`  Temperature: ${weather.main.temp}°C`);
    console.log(`  Feels like: ${weather.main.feels_like}°C`);
    console.log(`  Humidity: ${weather.main.humidity}%`);
    console.log(`  Conditions: ${weather.weather[0].description}`);
  } catch (error) {
    console.error('Failed to get weather:', error);
  }
}

main();

Sample Output

Weather in London:
  Temperature: 12°C
  Feels like: 10°C
  Humidity: 76%
  Conditions: scattered clouds

Storing API Keys Securely

Never hardcode API keys in your source code. Here is why and what to do instead.

The Problem

// NEVER DO THIS!
const API_KEY = 'sk_live_abc123xyz789'; // Anyone who sees your code has your key

// Your code gets pushed to GitHub
// Someone finds your key
// They use your API quota or access your data
// You get a huge bill or data breach

Solution 1: Environment Variables

Store keys in environment variables, not in code:

// Read from environment
const API_KEY = process.env.WEATHER_API_KEY;

if (!API_KEY) {
  throw new Error('WEATHER_API_KEY environment variable is required');
}

Set the variable before running:

# Linux/macOS
export WEATHER_API_KEY=your-api-key-here
node app.js

# Or inline
WEATHER_API_KEY=your-api-key-here node app.js
# Windows PowerShell
$env:WEATHER_API_KEY="your-api-key-here"
node app.js

Solution 2: .env Files

Use a .env file with the dotenv package:

# .env file (in your project root)
WEATHER_API_KEY=your-api-key-here
GITHUB_TOKEN=ghp_xxxxx
DATABASE_URL=postgres://localhost:5432/mydb
// At the top of your entry file
import 'dotenv/config';

// Now process.env has your variables
const API_KEY = process.env.WEATHER_API_KEY;

Critical: Add .env to your .gitignore:

# .gitignore
.env
.env.local
.env.*.local

Solution 3: Secret Management Services

For production applications, use dedicated secret managers:

  • AWS Secrets Manager
  • Google Secret Manager
  • HashiCorp Vault
  • Doppler

Handling API Key Errors

APIs return specific error codes for authentication problems:

401 Unauthorized

The key is missing or invalid:

async function fetchWithKey(url: string, apiKey: string) {
  const response = await fetch(url, {
    headers: { 'X-API-Key': apiKey },
  });

  if (response.status === 401) {
    throw new Error('Invalid API key. Please check your key and try again.');
  }

  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }

  return response.json();
}

403 Forbidden

The key is valid but lacks permission for this action:

if (response.status === 403) {
  throw new Error('Access denied. Your API key does not have permission for this resource.');
}

429 Too Many Requests

You have hit the rate limit:

if (response.status === 429) {
  // Check for retry-after header
  const retryAfter = response.headers.get('Retry-After');
  const waitTime = retryAfter ? parseInt(retryAfter, 10) : 60;

  throw new Error(`Rate limit exceeded. Try again in ${waitTime} seconds.`);
}

Complete Error Handler

interface ApiError {
  status: number;
  message: string;
  retryAfter?: number;
}

async function handleApiResponse(response: Response): Promise<unknown> {
  if (response.ok) {
    return response.json();
  }

  const error: ApiError = {
    status: response.status,
    message: 'Unknown error',
  };

  switch (response.status) {
    case 401:
      error.message = 'Invalid or missing API key';
      break;
    case 403:
      error.message = 'API key lacks required permissions';
      break;
    case 429:
      error.message = 'Rate limit exceeded';
      const retryAfter = response.headers.get('Retry-After');
      if (retryAfter) {
        error.retryAfter = parseInt(retryAfter, 10);
      }
      break;
    case 404:
      error.message = 'Resource not found';
      break;
    default:
      try {
        const body = await response.json();
        error.message = body.message || body.error || 'Request failed';
      } catch {
        error.message = `HTTP error ${response.status}`;
      }
  }

  throw error;
}

Best Practices

1. Use Different Keys for Development and Production

const API_KEY =
  process.env.NODE_ENV === 'production' ? process.env.PROD_API_KEY : process.env.DEV_API_KEY;

2. Rotate Keys Regularly

Change your API keys periodically, especially if:

  • A team member leaves
  • You suspect a leak
  • The key is old (yearly rotation is good practice)

3. Use Minimal Permissions

Request only the permissions you need:

// Instead of requesting full access
// Request read-only if you only need to read data

4. Monitor Usage

Check your API dashboard regularly for:

  • Unexpected usage spikes
  • Requests from unknown IPs
  • Failed authentication attempts

5. Have a Revocation Plan

Know how to quickly revoke a key if it is compromised:

  1. Log into provider dashboard
  2. Revoke compromised key
  3. Generate new key
  4. Update your application
  5. Redeploy

Exercises

Exercise 1: Environment Variables

Create a function that safely retrieves an API key from environment variables:

function getRequiredEnvVar(name: string): string {
  // Throw an error with a helpful message if the variable is missing
}

// Usage:
// const apiKey = getRequiredEnvVar("WEATHER_API_KEY");
Solution
function getRequiredEnvVar(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error(
      `Missing required environment variable: ${name}\n` +
        `Please set it before running the application:\n` +
        `  export ${name}=your-value-here`
    );
  }

  return value;
}

// Usage
try {
  const apiKey = getRequiredEnvVar('WEATHER_API_KEY');
  console.log('API key loaded successfully');
} catch (error) {
  console.error(error instanceof Error ? error.message : error);
  process.exit(1);
}

Exercise 2: API Client with Key

Create a reusable API client class that handles API key authentication:

class ApiClient {
  // Constructor should accept base URL and API key
  // Should have methods for GET requests
  // Should handle authentication errors
}

// Usage:
// const client = new ApiClient("https://api.example.com", "my-api-key");
// const data = await client.get("/users");
Solution
interface ApiClientOptions {
  baseUrl: string;
  apiKey: string;
  headerName?: string;
}

class ApiClient {
  private baseUrl: string;
  private apiKey: string;
  private headerName: string;

  constructor(options: ApiClientOptions) {
    this.baseUrl = options.baseUrl.replace(/\/$/, ''); // Remove trailing slash
    this.apiKey = options.apiKey;
    this.headerName = options.headerName || 'X-API-Key';
  }

  private getHeaders(): Record<string, string> {
    return {
      [this.headerName]: this.apiKey,
      'Content-Type': 'application/json',
    };
  }

  async get<T>(endpoint: string): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;

    const response = await fetch(url, {
      method: 'GET',
      headers: this.getHeaders(),
    });

    if (response.status === 401) {
      throw new Error('Invalid API key');
    }

    if (response.status === 403) {
      throw new Error('Access forbidden - check API key permissions');
    }

    if (response.status === 429) {
      throw new Error('Rate limit exceeded');
    }

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return response.json();
  }

  async post<T>(endpoint: string, data: unknown): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;

    const response = await fetch(url, {
      method: 'POST',
      headers: this.getHeaders(),
      body: JSON.stringify(data),
    });

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return response.json();
  }
}

// Usage example
async function main() {
  const client = new ApiClient({
    baseUrl: 'https://api.example.com',
    apiKey: process.env.API_KEY || '',
  });

  try {
    interface User {
      id: number;
      name: string;
    }

    const users = await client.get<User[]>('/users');
    console.log('Users:', users);
  } catch (error) {
    console.error('API error:', error);
  }
}

Exercise 3: Rate Limit Handler

Create a function that automatically retries requests when rate limited:

async function fetchWithRetry(
  url: string,
  options: RequestInit,
  maxRetries: number = 3
): Promise<Response> {
  // Should retry on 429 errors
  // Should respect Retry-After header
  // Should give up after maxRetries
}
Solution
function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries: number = 3
): Promise<Response> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url, options);

      if (response.status === 429) {
        if (attempt === maxRetries) {
          throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
        }

        // Get wait time from header or use exponential backoff
        const retryAfter = response.headers.get('Retry-After');
        let waitTime: number;

        if (retryAfter) {
          // Retry-After can be seconds or a date
          waitTime = parseInt(retryAfter, 10) * 1000;
          if (isNaN(waitTime)) {
            // It might be a date string
            waitTime = new Date(retryAfter).getTime() - Date.now();
          }
        } else {
          // Exponential backoff: 1s, 2s, 4s, 8s...
          waitTime = Math.pow(2, attempt) * 1000;
        }

        console.log(`Rate limited. Waiting ${waitTime / 1000}s before retry...`);
        await sleep(waitTime);
        continue;
      }

      return response;
    } catch (error) {
      lastError = error instanceof Error ? error : new Error(String(error));

      if (attempt === maxRetries) {
        throw lastError;
      }

      // Wait before retry on network errors
      await sleep(Math.pow(2, attempt) * 1000);
    }
  }

  throw lastError || new Error('Max retries exceeded');
}

// Usage
async function main() {
  const response = await fetchWithRetry('https://api.example.com/data', {
    headers: {
      'X-API-Key': 'your-key',
    },
  });

  const data = await response.json();
  console.log(data);
}

Key Takeaways

  1. API keys are unique strings that identify your application to an API
  2. Keys can be sent via query parameters, custom headers, or the Authorization header
  3. Never hardcode API keys in source code - use environment variables
  4. Add .env files to .gitignore to prevent accidental commits
  5. Handle 401 (invalid key), 403 (forbidden), and 429 (rate limit) errors appropriately
  6. Use different keys for development and production environments
  7. Rotate keys regularly and know how to revoke them quickly

Resources

Resource Type Level
MDN: HTTP Authentication Documentation Beginner
dotenv Documentation Documentation Beginner
OWASP API Security Guide Intermediate
Twelve-Factor App: Config Article Intermediate

Next Lesson

API keys are simple but limited. In the next lesson, we will explore Bearer tokens - a more powerful and flexible authentication method.

Continue to Lesson 6.2: Bearer Tokens