Skip to content

Protected Routes Guide

Complete guide to protecting your API routes with authentication middleware in Flowfull.

Overview

Flowfull provides flexible middleware patterns for route protection:

  • requireAuth() - Require authenticated user
  • optionalAuth() - Optional authentication
  • requireUserType() - Require specific user type
  • requirePermission() - Require specific permission

Basic Authentication

Require Authentication

Protect routes that require a logged-in user:

typescript
import { Hono } from 'hono';
import { requireAuth } from './lib/auth/middleware';

const app = new Hono();

// Protected route
app.get('/api/profile', requireAuth(), async (c) => {
  const userId = c.get('user_id');
  const email = c.get('email');
  
  const profile = await db.selectFrom('users')
    .where('id', '=', userId)
    .selectAll()
    .executeTakeFirst();
  
  return c.json({ profile });
});

Optional Authentication

For routes that work with or without authentication:

typescript
// Public route with personalization
app.get('/api/posts', optionalAuth(), async (c) => {
  const userId = c.get('user_id');  // May be undefined
  
  if (userId) {
    // Show personalized posts
    const posts = await db.selectFrom('posts')
      .where('user_id', '=', userId)
      .orWhere('visibility', '=', 'public')
      .selectAll()
      .execute();
    
    return c.json({ posts, personalized: true });
  } else {
    // Show public posts only
    const posts = await db.selectFrom('posts')
      .where('visibility', '=', 'public')
      .selectAll()
      .execute();
    
    return c.json({ posts, personalized: false });
  }
});

Role-Based Access

Require User Type

Protect routes for specific user types:

typescript
// Admin-only route
app.get('/api/admin/users', requireUserType('admin'), async (c) => {
  const users = await db.selectFrom('users').selectAll().execute();
  return c.json({ users });
});

// Teacher-only route
app.get('/api/teacher/classes', requireUserType('teacher'), async (c) => {
  const teacherId = c.get('user_id');
  const classes = await db.selectFrom('classes')
    .where('teacher_id', '=', teacherId)
    .selectAll()
    .execute();
  
  return c.json({ classes });
});

// Student-only route
app.get('/api/student/assignments', requireUserType('student'), async (c) => {
  const studentId = c.get('user_id');
  const assignments = await db.selectFrom('assignments')
    .where('student_id', '=', studentId)
    .selectAll()
    .execute();
  
  return c.json({ assignments });
});

Dynamic Role Routing

Route users to different dashboards based on role:

typescript
app.get('/api/dashboard', requireAuth(), async (c) => {
  const userType = c.get('user_type');
  
  switch (userType) {
    case 'admin':
      return c.json({ 
        dashboard: await getAdminDashboard(),
        type: 'admin'
      });
    
    case 'teacher':
      return c.json({ 
        dashboard: await getTeacherDashboard(c.get('user_id')),
        type: 'teacher'
      });
    
    case 'student':
      return c.json({ 
        dashboard: await getStudentDashboard(c.get('user_id')),
        type: 'student'
      });
    
    default:
      return c.json({ error: 'Invalid user type' }, 403);
  }
});

Permission-Based Access

Require Permission

Protect routes with granular permissions:

typescript
// Require specific permission
app.delete('/api/posts/:id', requirePermission('posts.delete'), async (c) => {
  const postId = c.req.param('id');
  
  await db.deleteFrom('posts')
    .where('id', '=', postId)
    .execute();
  
  return c.json({ success: true });
});

// Multiple permissions
app.post('/api/admin/users', 
  requirePermission('users.create'),
  requirePermission('admin.access'),
  async (c) => {
    const data = await c.req.json();
    const user = await createUser(data);
    return c.json({ user });
  }
);

Validation Modes

Use different validation modes for different security levels:

typescript
// Standard validation (IP check)
app.get('/api/profile', requireAuth('STANDARD'), async (c) => {
  // ...
});

// Advanced validation (IP + User-Agent)
app.get('/api/billing', requireAuth('ADVANCED'), async (c) => {
  // ...
});

// Strict validation (IP + User-Agent + Device ID)
app.post('/api/transfer', requireAuth('STRICT'), async (c) => {
  // ...
});

Learn more about Validation Modes →

Context Variables

After authentication, these variables are available:

typescript
app.get('/api/me', requireAuth(), async (c) => {
  const userId = c.get('user_id');           // User ID
  const email = c.get('email');              // Email
  const userType = c.get('user_type');       // User type
  const sessionId = c.get('session_id');     // Session ID
  const permissions = c.get('permissions');  // Permissions array
  
  return c.json({ 
    userId, 
    email, 
    userType,
    permissions 
  });
});

Custom Middleware

Create custom middleware for specific needs:

Require Email Verification

typescript
export function requireVerifiedEmail() {
  return async (c: Context, next: Next) => {
    const userId = c.get('user_id');
    
    const user = await db.selectFrom('users')
      .where('id', '=', userId)
      .select('email_verified')
      .executeTakeFirst();
    
    if (!user?.email_verified) {
      return c.json({ 
        error: 'Email not verified',
        code: 'EMAIL_NOT_VERIFIED'
      }, 403);
    }
    
    await next();
  };
}

// Usage
app.get('/api/premium', 
  requireAuth(), 
  requireVerifiedEmail(), 
  async (c) => {
    // Premium content
  }
);

Require Organization Membership

typescript
export function requireOrgMember(orgId?: string) {
  return async (c: Context, next: Next) => {
    const userId = c.get('user_id');
    const targetOrgId = orgId || c.req.param('orgId');
    
    const membership = await db.selectFrom('org_members')
      .where('user_id', '=', userId)
      .where('organization_id', '=', targetOrgId)
      .selectAll()
      .executeTakeFirst();
    
    if (!membership) {
      return c.json({ 
        error: 'Not a member of this organization' 
      }, 403);
    }
    
    c.set('org_role', membership.role);
    await next();
  };
}

// Usage
app.get('/api/orgs/:orgId/settings', 
  requireAuth(), 
  requireOrgMember(), 
  async (c) => {
    const orgId = c.req.param('orgId');
    const role = c.get('org_role');
    // ...
  }
);

Chaining Middleware

Combine multiple middleware for complex requirements:

typescript
app.post(
  '/api/admin/users',
  requireAuth('STRICT'),           // Strict validation
  requireUserType('admin'),        // Must be admin
  requirePermission('users.create'), // Must have permission
  requireVerifiedEmail(),          // Must have verified email
  async (c) => {
    // Create user
    const data = await c.req.json();
    const user = await createUser(data);
    return c.json({ user });
  }
);

Error Handling

Handle authentication errors gracefully:

typescript
app.get('/api/protected', requireAuth(), async (c) => {
  try {
    const data = await getData(c.get('user_id'));
    return c.json({ data });
  } catch (error) {
    console.error('Error:', error);
    return c.json({ 
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    }, 500);
  }
});

// Global error handler
app.onError((err, c) => {
  if (err.message === 'Unauthorized') {
    return c.json({ 
      error: 'Authentication required',
      code: 'UNAUTHORIZED'
    }, 401);
  }
  
  return c.json({ 
    error: 'Internal server error',
    code: 'INTERNAL_ERROR'
  }, 500);
});

Real-World Examples

E-commerce API

typescript
// Public product listing
app.get('/api/products', optionalAuth(), async (c) => {
  const userId = c.get('user_id');
  const products = await getProducts(userId);
  return c.json({ products });
});

// Protected cart
app.get('/api/cart', requireAuth(), async (c) => {
  const cart = await getCart(c.get('user_id'));
  return c.json({ cart });
});

// Checkout (strict validation)
app.post('/api/checkout', requireAuth('STRICT'), async (c) => {
  const order = await processCheckout(c.get('user_id'));
  return c.json({ order });
});

// Admin product management
app.post('/api/admin/products', 
  requireUserType('admin'), 
  async (c) => {
    const product = await createProduct(await c.req.json());
    return c.json({ product });
  }
);

SaaS Application

typescript
// Public features
app.get('/api/features', async (c) => {
  return c.json({ features: await getFeatures() });
});

// User dashboard
app.get('/api/dashboard', requireAuth(), async (c) => {
  const dashboard = await getDashboard(c.get('user_id'));
  return c.json({ dashboard });
});

// Organization settings
app.get('/api/org/:orgId/settings', 
  requireAuth(),
  requireOrgMember(),
  async (c) => {
    const settings = await getOrgSettings(c.req.param('orgId'));
    return c.json({ settings });
  }
);

// Billing (strict validation)
app.post('/api/billing/subscribe', 
  requireAuth('STRICT'),
  requireVerifiedEmail(),
  async (c) => {
    const subscription = await createSubscription(c.get('user_id'));
    return c.json({ subscription });
  }
);

Best Practices

✅ Do

  • Use requireAuth() for protected routes
  • Use optionalAuth() for public routes with personalization
  • Use appropriate validation modes
  • Handle errors gracefully
  • Chain middleware for complex requirements
  • Create reusable custom middleware

❌ Don't

  • Skip authentication on sensitive routes
  • Use DISABLED mode in production
  • Ignore authentication errors
  • Hardcode user types in routes
  • Forget to validate permissions
  • Mix authentication logic in route handlers

Next Steps

Need Help?

Released under the MIT License.