Security Fundamentals

Securing APIs and Endpoints

APIs are attacked programmatically at scale — learn the complete set of protections every API needs.

API Security Is Different

APIs are different from web pages in one critical way: they are attacked programmatically, at scale, without a human in the loop. While a web page might be probed by a human a few times per minute, an API endpoint might receive thousands of automated attack requests per second.

Every API endpoint is a potential attack surface. Treat each one with the same discipline you'd apply to a bank vault.

API Key Management

For service-to-service communication, use API keys:

typescript
// Generate a cryptographically secure API key
import { randomBytes } from 'crypto';
const apiKey = randomBytes(32).toString('hex');

// Store the HASH of the key (never the key itself)
import { createHash } from 'crypto';
const keyHash = createHash('sha256').update(apiKey).digest('hex');
await db.apiKeys.create({ hash: keyHash, userId, scopes: ['read'] });

// Validate incoming keys
export async function validateApiKey(providedKey: string) {
  const hash = createHash('sha256').update(providedKey).digest('hex');
  return db.apiKeys.findByHash(hash);
}

Key principles:

  • Pass keys in the Authorization header (never in URLs — URLs appear in logs)
  • Store only the hash (like passwords)
  • Scope keys to minimum required permissions
  • Rotate keys regularly

Rate Limiting

Rate limiting prevents brute force attacks and API abuse:

typescript
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '1 m'), // 10 requests per minute
});

export async function middleware(req: NextRequest) {
  const ip = req.ip ?? '127.0.0.1';
  const { success, limit, remaining } = await ratelimit.limit(ip);

  if (!success) {
    return new Response('Too Many Requests', {
      status: 429,
      headers: {
        'X-RateLimit-Limit': limit.toString(),
        'X-RateLimit-Remaining': remaining.toString(),
        'Retry-After': '60',
      },
    });
  }
}

Response Security

Never leak internal details in API error responses:

typescript
// DANGEROUS — leaks stack trace and internal details
catch (error) {
  return Response.json({ error: error.message, stack: error.stack }, { status: 500 });
}

// SECURE — generic message to client, full details logged internally
catch (error) {
  console.error('API error:', { error, userId, requestId });
  return Response.json(
    { error: 'An unexpected error occurred', requestId },
    { status: 500 }
  );
}

Webhook Security

Webhooks from external services (Stripe, GitHub) must be verified:

typescript
import Stripe from 'stripe';

export async function POST(req: Request) {
  const body = await req.text();
  const signature = req.headers.get('stripe-signature') ?? '';

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return Response.json({ error: 'Invalid signature' }, { status: 400 });
  }

  // Now safe to process the event
}

The signature verification ensures the request genuinely came from Stripe and was not tampered with.

Key Takeaways

  • APIs are attacked programmatically — rate limiting is not optional
  • API keys belong in the Authorization header, not in URLs
  • Store only the hash of API keys — never the keys themselves
  • Error responses should be generic to the client but detailed in your logs
  • Verify webhook signatures using HMAC to ensure requests genuinely came from the expected source

Example

typescript
// API route with full security: auth + rate limiting + validation
import { z } from 'zod';

const CreateSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().max(10000),
});

export async function POST(req: Request) {
  // 1. Authentication
  const token = req.headers.get('authorization')?.replace('Bearer ', '');
  if (!token) return Response.json({ error: 'Unauthorized' }, { status: 401 });

  const user = await verifyToken(token);
  if (!user) return Response.json({ error: 'Invalid token' }, { status: 401 });

  // 2. Rate limiting (checked in middleware)

  // 3. Input validation
  const body = await req.json();
  const result = CreateSchema.safeParse(body);
  if (!result.success) {
    return Response.json({ errors: result.error.flatten() }, { status: 400 });
  }

  // 4. Business logic with parameterized queries
  const post = await db.posts.create({ ...result.data, userId: user.id });

  // 5. Safe response (no internal details)
  return Response.json({ id: post.id, title: post.title }, { status: 201 });
}
Try it yourself — TYPESCRIPT

Docker, AWS, Vercel, Netlify, GitHub, GitHub Actions are trademarks of Docker, Inc., Amazon.com, Inc., Vercel, Inc., Netlify, Inc., Microsoft Corporation. DevForge Academy is not affiliated with, endorsed by, or sponsored by these companies. Referenced for educational purposes only. See full disclaimers