API Protection

The boilerplate provides two approaches for protecting API routes: native Better Auth integration and custom withAuth helpers for cleaner code.
Perfect for: Protecting sensitive data, user management APIs, payment endpoints, and admin functions.

Method 1: Native Better Auth Protection

Basic Authentication Check

// Direct Better Auth usage
import { auth } from '@/lib/better-auth/auth'
import { NextRequest } from 'next/server'
import { headers } from 'next/headers'

export async function GET(request: NextRequest) {
  // Check if user is authenticated
  const session = await auth.api.getSession({
    headers: await headers()
  })

  if (!session?.session?.userId) {
    return Response.json({ error: 'Non authentifié' }, { status: 401 })
  }

  // User is authenticated
  return Response.json({
    message: 'This is protected data',
    userId: session.session.userId
  })
}

Role-Based Check with Better Auth

// With manual role checking
import { auth } from '@/lib/better-auth/auth'
import { getAuthUser } from '@/services/authentication/auth-service'
import { hasRequiredRole } from '@/services/authentication/auth-util'
import { RoleConst } from '@/services/types/domain/auth-types'

export async function GET(request: NextRequest) {
  const session = await auth.api.getSession({
    headers: await headers()
  })

  if (!session?.session?.userId) {
    return Response.json({ error: 'Non authentifié' }, { status: 401 })
  }

  // Get full user with role
  const authUser = await getAuthUser()
  if (!authUser) {
    return Response.json({ error: 'Utilisateur non trouvé' }, { status: 404 })
  }

  // Check role
  const hasRole = hasRequiredRole(authUser, RoleConst.ADMIN)
  if (!hasRole) {
    return Response.json({ error: 'Non autorisé' }, { status: 403 })
  }

  // Admin logic here
  return Response.json({ data: 'Admin data' })
}
The boilerplate provides clean helper functions that handle authentication and role checking automatically.

Static Routes Protection

// src/app/api/projects/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { withAuth, withUserAuth } from '@/lib/api-auth'
import { RoleConst } from '@/services/types/domain/auth-types'

// Basic protection with custom role
export const GET = withAuth(async (request: NextRequest, authUser) => {
  // User is authenticated and has USER role (or higher)
  // authUser contains full user data including role

  const result = await getProjectsService(authUser.id)
  return NextResponse.json({ success: true, data: result })
}, RoleConst.USER)

// Shortcut for USER role
export const POST = withUserAuth(async (request: NextRequest, authUser) => {
  const body = await request.json()

  const newProject = await createProjectService({
    ...body,
    createdBy: authUser.id
  })

  return NextResponse.json({ success: true, data: newProject }, { status: 201 })
})

Dynamic Routes Protection

// src/app/api/projects/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { withDynamicUserAuth } from '@/lib/api-auth'

// For routes with dynamic parameters
export const GET = withDynamicUserAuth(
  async (request: NextRequest, authUser, context) => {
    const params = await context.params
    const projectId = params.id

    const project = await getProjectByIdService(projectId)
    return NextResponse.json({ success: true, data: project })
  }
)

export const PUT = withDynamicUserAuth(
  async (request: NextRequest, authUser, context) => {
    const params = await context.params
    const projectId = params.id
    const body = await request.json()

    const updatedProject = await updateProjectService({
      id: projectId,
      ...body
    })

    return NextResponse.json({
      success: true,
      message: 'Projet mis à jour avec succès',
      data: updatedProject
    })
  }
)

Available Helper Functions

import {
  withAuth,           // Custom role
  withUserAuth,       // USER role shortcut
  withAdminAuth,      // ADMIN role shortcut
  withRedactorAuth    // REDACTOR role shortcut
} from '@/lib/api-auth'

// Usage examples
export const GET = withAuth(async (request, authUser) => {
  // Logic here
  return NextResponse.json({ data: 'moderator data' })
}, RoleConst.MODERATOR)

export const POST = withUserAuth(async (request, authUser) => {
  // Logic here
  return NextResponse.json({ success: true })
})

export const DELETE = withAdminAuth(async (request, authUser) => {
  // Logic here
  return NextResponse.json({ success: true })
})

export const PUT = withRedactorAuth(async (request, authUser) => {
  // Logic here
  return NextResponse.json({ success: true })
})

Comparison: Which Method to Choose?

Advantages:
  • Cleaner code - No boilerplate auth logic
  • Consistent handling - Standardized error responses
  • Type safety - authUser parameter is fully typed
  • Role shortcuts - withUserAuth, withAdminAuth, etc.
  • Less repetition - DRY principle
Best for: Most API routes in your application
Recommendation: Use withAuth helpers for 95% of your API routes. Only use native Better Auth when you need custom authentication logic or debugging.

Error Responses

Both methods return standardized JSON error responses:
// 401 Unauthorized
{
  "error": "Non authentifié"
}

// 403 Forbidden
{
  "error": "Non autorisé"
}

// 404 User Not Found
{
  "error": "Utilisateur non trouvé"
}

Testing API Protection

1

Test unauthenticated access

  1. Make API requests without session cookies
  2. Verify 401 "Non authentifié" responses
  3. Check error message formatting
2

Test role-based endpoints

  1. Test with users of different roles (USER, ADMIN, etc.)
  2. Verify role hierarchy works correctly
  3. Check 403 "Non autorisé" for insufficient roles
3

Test dynamic routes

  1. Test routes with [id] parameters
  2. Verify context.params access works
  3. Test parameter validation
APIs secured! Your backend endpoints are now protected with both native Better Auth and convenient helper functions.
    API Protection | ShipSaaS Documentation | ShipSaaS - Launch your SaaS with AI in days