-
Notifications
You must be signed in to change notification settings - Fork 2
Routes
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.
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.
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
.
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.
New team members can understand your API structure in seconds just by looking at the file tree. No domain knowledge required.
// 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 };
}
};
// 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"
.
// 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 };
}
};
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:
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;
}
};
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
becomesPOST /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
// 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()
};
}
};
routes/
├── v1/
│ └── users/
│ └── get.js ← GET /v1/users
└── v2/
└── users/
└── get.js ← GET /v2/users (improved version)
// 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
}
}
};
// 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);
# 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.
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();
- Connection pooling: Handled automatically
- Query caching: Built into ArangoDB
- Transaction management: Simplified and optimized
- Memory efficiency: Shared V8 contexts
- Faster Onboarding: New developers understand the codebase immediately
- Fewer Bugs: Less configuration means fewer places for errors
- Better Maintainability: Related code stays together naturally
- 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.
Copyright © 2016-2025, Skitsanos™