Skip to content
skitsanos edited this page Jul 20, 2025 · 3 revisions

API Routes: File-Based Routing Done Right

Intuitive routing that feels like magic, but is actually just good design

If you've ever used Next.js, you already know how to create routes in Foxx Builder. But if you haven't, prepare to be amazed at how natural this feels.

The Core Insight: Files = Endpoints

Traditional backend frameworks make you register routes manually:

// Traditional Express.js approach ❌
app.get('/users', getUsersHandler);
app.post('/users', createUserHandler);  
app.get('/users/:id', getUserHandler);
app.put('/users/:id', updateUserHandler);
app.delete('/users/:id', deleteUserHandler);

// Where do these handlers live? Who knows! 🤷‍♂️

Foxx Builder's approach:

routes/
├── users/
│   ├── get.js      ← GET /users
│   ├── post.js     ← POST /users  
│   └── [id]/
│       ├── get.js  ← GET /users/:id
│       ├── put.js  ← PUT /users/:id
│       └── delete.js ← DELETE /users/:id

The magic: Your file structure IS your API structure. No configuration, no routing tables, no confusion.

Why This Changes Everything

🧠 Mental Model Alignment

Your API documentation, your file explorer, and your actual endpoints all match perfectly. When someone asks "where's the user creation endpoint?", you point to routes/users/post.js.

📁 Organized by Default

Related endpoints naturally group together. User management? It's all in the users/ folder. Authentication? Check the auth/ folder. No more hunting through massive routing files.

🔍 Instant Understanding

New team members can understand your API structure in seconds just by looking at the file tree. No domain knowledge required.

From Simple to Sophisticated

Level 1: Basic CRUD Operations

// routes/users/get.js - List all users
module.exports = {
  name: 'List Users',
  description: 'Get all users in the system',
  handler: (req, res) => {
    const users = req.db.collection('users').all().toArray();
    return { users, count: users.length };
  }
};
// routes/users/post.js - Create a user
module.exports = {
  name: 'Create User',
  description: 'Add a new user to the system',
  body: {
    type: 'object',
    properties: {
      username: { type: 'string', minLength: 3 },
      email: { type: 'string', format: 'email' }
    },
    required: ['username', 'email']
  },
  handler: (req, res) => {
    const collection = req.db.collection('users');
    const newUser = collection.save(req.body);
    return { success: true, userId: newUser._key };
  }
};

Level 2: Dynamic Routes with Parameters

// routes/users/[id]/get.js - Get specific user
module.exports = {
  name: 'Get User',
  description: 'Retrieve a specific user by ID',
  handler: (req, res) => {
    const { id } = req.pathParams;
    const user = req.db.collection('users').document(id);
    return { user };
  }
};

The [id] folder automatically captures URL parameters. So /users/123 becomes req.pathParams.id = "123".

Level 3: Complex Nested Resources

// routes/users/[userId]/posts/[postId]/comments/get.js
// Handles: GET /users/123/posts/456/comments
module.exports = {
  name: 'Get Post Comments',
  handler: (req, res) => {
    const { userId, postId } = req.pathParams;
    
    // Validate user exists
    const user = req.db.collection('users').document(userId);
    
    // Get comments for this post
    const comments = req.db.query`
      FOR comment IN comments
        FILTER comment.postId == ${postId}
        FILTER comment.userId == ${userId}
        RETURN comment
    `.toArray();
    
    return { comments, user: user.username };
  }
};

Real-World Example: Secure Authentication

Let's build a complete authentication system to show how powerful this approach really is. Here's a production-ready login endpoint that handles everything you need:

The Power of Context Extensions

Notice how this example uses Foxx Builder's context extensions (auth, update, utils) - these are helper functions that eliminate boilerplate and make your code cleaner and more secure.

// routes/auth/login/post.js
const joi = require('joi');
const {query} = require('@arangodb');
const crypto = require('@arangodb/crypto');

module.exports = {
    name: 'User Login',
    description: 'Authenticate user and return session token',

    body: {
        model: joi.object({
            username: joi.string().required(),
            password: joi.string().required()
        }).required()
    },

    handler: (req, res) => {
        const {utils, update, auth} = module.context;
        const {username, password} = req.body;

        // Find user with matching credentials
        const [queryResult] = query`
            FOR doc IN users
            FILTER 
                doc.username == ${username}
                AND
                doc.password == ${crypto.sha384(password)}
            RETURN unset(doc, "_id", "_rev", "password")`
        .toArray();

        if (!queryResult) {
            res.throw(403, 'Invalid username or password');
        }

        // Update last login timestamp
        update('users', queryResult._key, {
            lastLogin: new Date().toISOString()
        });

        // Build response with user data and session token
        const response = {
            user: queryResult,
            session: {
                token: auth.encode({
                    userId: queryResult._key,
                    expiresIn: '7d'
                })
            }
        };

        // Add gravatar if username is email
        if (utils.isEmail(username)) {
            response.user.gravatar = `https://www.gravatar.com/avatar/${crypto.md5(username)}?d=robohash&s=150`;
        }

        return response;
    }
};

What Makes This Approach Superior

In traditional frameworks, this would require:

  • ❌ Manual route registration
  • ❌ Separate validation middleware
  • ❌ Custom authentication helpers
  • ❌ Manual error handling
  • ❌ Response formatting boilerplate

With Foxx Builder:

  • File location = endpoint: routes/auth/login/post.js becomes POST /auth/login
  • Built-in validation: The body schema automatically validates requests
  • Context extensions: auth.encode(), update(), utils.isEmail() handle common tasks
  • AQL integration: Native database queries with template literals
  • Auto-documentation: API docs generated from your route definitions

Advanced Patterns That Scale

Middleware-Style Route Guards

// routes/admin/users/get.js
module.exports = {
  name: 'Admin: List Users',
  description: 'Admin-only endpoint to list all users',
  
  // Built-in authentication requirement
  requiresAuth: true,
  requiresRole: 'admin',
  
  handler: (req, res) => {
    // req.user is automatically available (populated by auth middleware)
    const users = req.db.collection('users').all().toArray();
    
    return {
      users,
      requestedBy: req.user.username,
      timestamp: new Date().toISOString()
    };
  }
};

Automatic API Versioning

routes/
├── v1/
│   └── users/
│       └── get.js     ← GET /v1/users
└── v2/
    └── users/
        └── get.js     ← GET /v2/users (improved version)

Smart Error Handling

// routes/users/[id]/get.js
module.exports = {
  name: 'Get User',
  
  handler: (req, res) => {
    const { id } = req.pathParams;
    
    try {
      const user = req.db.collection('users').document(id);
      return { user };
    } catch (error) {
      if (error.isArangoError && error.errorNum === 1202) {
        // Document not found
        res.throw(404, `User ${id} not found`);
      }
      throw error; // Re-throw unexpected errors
    }
  }
};

The Developer Experience Difference

Before Foxx Builder:

// Typical Express.js setup
const express = require('express');
const app = express();

// Manual route registration (gets messy fast)
app.get('/users', require('./handlers/users/list'));
app.post('/users', require('./handlers/users/create'));
app.get('/users/:id', require('./handlers/users/get'));
app.put('/users/:id', require('./handlers/users/update'));
// ... 50+ more routes

// Don't forget middleware!
app.use('/admin/*', requireAuth);
app.use('/admin/*', requireRole('admin'));

app.listen(3000);

With Foxx Builder:

# Just create files. That's it.
touch routes/users/get.js
touch routes/users/post.js  
touch routes/users/[id]/get.js
touch routes/users/[id]/put.js

# Deploy
task deploy-docker

Result: You spend time building features, not configuring frameworks.

Performance Benefits

Database Co-location

Since your API runs inside ArangoDB, there's zero network latency between your logic and data:

// This query executes instantly (no network round-trip)
const users = req.db.query`
  FOR user IN users
    FILTER user.active == true
    SORT user.createdAt DESC
    LIMIT 10
    RETURN user
`.toArray();

Automatic Optimizations

  • Connection pooling: Handled automatically
  • Query caching: Built into ArangoDB
  • Transaction management: Simplified and optimized
  • Memory efficiency: Shared V8 contexts

Why This Matters for Your Team

  1. Faster Onboarding: New developers understand the codebase immediately
  2. Fewer Bugs: Less configuration means fewer places for errors
  3. Better Maintainability: Related code stays together naturally
  4. Improved Productivity: Focus on business logic, not framework boilerplate

Ready to see how validation works with this routing system? Check out Validating Payload to learn about Foxx Builder's powerful validation features.

Clone this wiki locally