Security Guide
HomeBlogAPI Security Essentials
Security

API Security Essentials: 5 Mistakes That Could Expose Your Backend

Naveen Sharma
December 1, 2025
8 min read

Secure Your APIs

Before Someone Else Does

Last month, a client called me in a panic. Their API was getting hammered with requests, their Supabase bill shot up to $800 overnight, and someone had scraped their entire product database.

The problem? They'd built a solid backend, but forgot to add basic security layers. No rate limiting, API keys exposed in frontend code, and zero input validation.

I've seen this happen way too many times. Developers focus on shipping features fast — which is great — but security often gets pushed to "later." And "later" never comes until something breaks.

Here are 5 common API security mistakes I keep catching in code reviews, and how to fix them before they become expensive problems.

1Exposing API Keys in Your Frontend Code

The Problem

I see this constantly: developers hardcode Supabase keys, Stripe keys, or API tokens directly in React components or client-side JavaScript. Anyone can open DevTools and steal them.

A few months ago, I audited a startup's codebase and found their Supabase anon key sitting right in a Next.js component. That key had full read access to their database. Anyone could query their entire user table.

Don't Do This:
// ❌ BAD - Never do this
const supabaseUrl = 'https://your-project.supabase.co'
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'

// This is visible to anyone who inspects your code
The Fix

Use environment variables for anything sensitive, and create API routes to proxy requests:

// ✅ GOOD - Use environment variables
// .env.local (never commit this!)
NEXT_PUBLIC_SUPABASE_URL=your-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

// For sensitive operations, use API routes
// app/api/users/route.ts
export async function GET(request: Request) {
  const apiKey = process.env.SUPABASE_SERVICE_KEY // Server-only
  // Handle request securely
}

Pro tip: Even with environment variables, assume anything with NEXT_PUBLIC_ prefix is public. Use API routes for anything sensitive.

2No Rate Limiting (Letting Bots Drain Your Budget)

This is the mistake that cost my client $800. Without rate limiting, someone can hit your API endpoints thousands of times per second. Your database queries explode, your hosting bill spikes, and your real users get locked out.

Real Example

A client's product search endpoint was getting 200+ requests per second from a single IP. Their Supabase database hit the connection limit, and the app went down for 3 hours during peak traffic.

Rate limiting doesn't have to be complicated. Here's a simple approach using Next.js middleware:

Quick Implementation:
// middleware.ts
import { NextResponse } from 'next/server'

const rateLimitMap = new Map()

export function middleware(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown'
  const limit = 10 // requests
  const window = 60000 // 1 minute

  const key = `${ip}-${request.url}`
  const now = Date.now()
  const requests = rateLimitMap.get(key) || []

  // Clean old requests
  const recentRequests = requests.filter(
    (time: number) => now - time < window
  )

  if (recentRequests.length >= limit) {
    return NextResponse.json(
      { error: 'Too many requests' },
      { status: 429 }
    )
  }

  recentRequests.push(now)
  rateLimitMap.set(key, recentRequests)

  return NextResponse.next()
}

export const config = {
  matcher: '/api/:path*'
}
Better Option

For production, use a service like Upstash Redis or Vercel's built-in rate limiting. But even a simple in-memory solution is better than nothing.

3Trusting User Input Without Validation

This one's classic, but I still see it everywhere. Developers accept whatever data comes from the frontend and shove it straight into database queries. That's how SQL injection attacks happen.

Dangerous Pattern
// ❌ BAD - Never trust user input
const userId = req.body.userId
const query = `SELECT * FROM users WHERE id = '${userId}'`

// Someone could send: userId = "1' OR '1'='1"
// And suddenly they're querying all users

The good news? If you're using Supabase or any modern ORM, you're mostly protected from SQL injection. But you still need to validate data types, lengths, and formats.

The Right Way
// ✅ GOOD - Validate everything
import { z } from 'zod'

const userSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().min(18).max(120)
})

// In your API route
const result = userSchema.safeParse(req.body)
if (!result.success) {
  return Response.json(
    { error: result.error.errors },
    { status: 400 }
  )
}

// Now you can safely use result.data

Use Zod or Yup for validation. It catches bad data before it hits your database, and gives you clear error messages.

4Missing Authentication Checks on Sensitive Endpoints

I reviewed a codebase last year where the "delete user" endpoint was public. No authentication check, no authorization — just a simple DELETE request and poof, user gone.

Even if you have auth on your frontend, always verify it on the backend. Frontend code can be modified. Backend is your last line of defense.

Always Verify on Backend:
// app/api/users/[id]/route.ts
import { createClient } from '@supabase/supabase-js'

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  // ✅ Check authentication
  const authHeader = request.headers.get('authorization')
  if (!authHeader) {
    return Response.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  // Verify the token
  const supabase = createClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_SERVICE_KEY!
  )

  const { data: { user }, error } = await supabase.auth.getUser(
    authHeader.replace('Bearer ', '')
  )

  if (error || !user) {
    return Response.json(
      { error: 'Invalid token' },
      { status: 401 }
    )
  }

  // ✅ Check authorization (can this user delete this resource?)
  if (user.id !== params.id && user.role !== 'admin') {
    return Response.json(
      { error: 'Forbidden' },
      { status: 403 }
    )
  }

  // Now safe to delete
  // ... deletion logic
}
Remember

Two checks: Authentication (who are you?) and Authorization (can you do this?). Both matter.

5Exposing Sensitive Data in Error Messages

Error messages are helpful for debugging, but they shouldn't leak information to attackers. I've seen APIs return full database schemas, API keys, and file paths in error responses.

Information Leak
// ❌ BAD - Don't expose internals
catch (error) {
  return Response.json({
    error: `Database error: ${error.message}`,
    query: error.query, // Exposes your SQL!
    stack: error.stack  // Exposes file paths!
  })
}
Safe Error Handling
// ✅ GOOD - Generic errors for users
catch (error) {
  // Log full error server-side
  console.error('Database error:', error)
  
  // Return generic message to client
  return Response.json(
    { error: 'Something went wrong. Please try again.' },
    { status: 500 }
  )
}

// In development, you can be more verbose
if (process.env.NODE_ENV === 'development') {
  return Response.json({
    error: error.message,
    stack: error.stack
  })
}

Log detailed errors server-side for debugging, but keep client responses generic. Attackers can't exploit what they can't see.

🔒 Security Checklist for Your Next API

Use environment variables for all secrets
Implement rate limiting on all endpoints
Validate and sanitize all user input
Verify authentication on every protected route
Check authorization before sensitive operations
Use generic error messages in production
Enable CORS only for trusted domains
Use HTTPS everywhere (no exceptions)
Keep dependencies updated (check for vulnerabilities)
Set up monitoring and alerts for suspicious activity

Security doesn't have to be complicated. Most breaches happen because of these basic mistakes — not sophisticated attacks. Fix these five things, and you're already ahead of 80% of APIs out there.

Start with rate limiting and input validation. Those two alone will prevent most common attacks. Then add authentication checks, and you're in good shape.

Need Help Securing Your Backend?

I help startups build secure, scalable APIs without the complexity. Let's audit your current setup or build something new the right way.