veloxapi
Version:
An ultra-fast, zero-dependency Node.js web framework built entirely with Node.js built-in modules
685 lines (520 loc) • 16.7 kB
Markdown
# Advanced Typed Routing
VeloxAPI supports **multiple routes with the same path but different parameter types**, enabling powerful routing patterns based on data validation.
## Table of Contents
1. [Basic Typed Parameters](#basic-typed-parameters)
2. [Multiple Types for Same Parameter](#multiple-types-for-same-parameter)
3. [How It Works](#how-it-works)
4. [All Parameter Types](#all-parameter-types)
5. [Advanced Patterns](#advanced-patterns)
6. [Best Practices](#best-practices)
7. [Performance](#performance)
---
## Basic Typed Parameters
Define parameter types directly in your routes:
```javascript
import { VeloxRouter } from 'veloxapi';
const router = new VeloxRouter();
// Automatically validates and converts to number
router.get('/users/:id=number', (res, req, query, params) => {
console.log(typeof params.id); // 'number'
res.sendJSON({ userId: params.id });
});
```
**Request handling:**
- ✅ `/users/123` → Matches, params.id = 123 (number)
- ❌ `/users/abc` → 404 (validation fails)
---
## Multiple Types for Same Parameter
**NEW in v0.2.0:** You can define **multiple routes with the same path but different parameter types**!
### Example: String vs Number
```javascript
router.get('/users/:id=string', (res, req, query, params) => {
// Handles string IDs (usernames, slugs)
res.sendJSON({
type: 'string',
id: params.id
});
});
router.get('/users/:id=number', (res, req, query, params) => {
// Handles numeric IDs
res.sendJSON({
type: 'number',
id: params.id
});
});
```
**Request routing:**
- `/users/123` → Number route (params.id = 123)
- `/users/alice` → String route (params.id = "alice")
- `/users/123abc` → String route (params.id = "123abc")
### Example: UUID vs Slug
```javascript
router.get('/posts/:id=uuid', (res, req, query, params) => {
// Handles UUID identifiers
res.sendJSON({
type: 'uuid',
id: params.id
});
});
router.get('/posts/:id=slug', (res, req, query, params) => {
// Handles slug identifiers
res.sendJSON({
type: 'slug',
id: params.id
});
});
```
**Request routing:**
- `/posts/550e8400-e29b-41d4-a716-446655440000` → UUID route
- `/posts/my-blog-post-2024` → Slug route
### Example: Int vs Float
```javascript
router.get('/products/:price=int', (res, req, query, params) => {
// Handles whole numbers
res.sendJSON({
price: params.price,
isInteger: true
});
});
router.get('/products/:price=float', (res, req, query, params) => {
// Handles decimal numbers
res.sendJSON({
price: params.price,
isInteger: false
});
});
```
**Request routing:**
- `/products/100` → Int route (params.price = 100)
- `/products/99.99` → Float route (params.price = 99.99)
---
## How It Works
### 1. Route Registration
When you register routes with typed parameters, VeloxAPI creates **separate tree branches** for each type:
```javascript
router.get('/items/:id=number', handler1); // Branch 1: :id=number
router.get('/items/:id=string', handler2); // Branch 2: :id=string
```
**Internal structure:**
```
/items
├── :id=number → handler1
└── :id=string → handler2
```
### 2. Request Matching
When a request comes in, VeloxAPI:
1. **Validates** the parameter against each type
2. **Matches** the first valid route
3. **Converts** the parameter to the correct type
4. **Returns 404** if no types match
**Example:**
```
Request: /items/123
1. Check :id=number → validateParam("123", "number") → ✅ Match!
2. Convert: params.id = 123 (number)
3. Execute handler1
```
### 3. Validation Priority
Routes are matched in the order they validate successfully:
```javascript
// More specific types should be defined first
router.get('/data/:val=int', intHandler); // Matches 123
router.get('/data/:val=float', floatHandler); // Matches 123.45
router.get('/data/:val=number', numberHandler); // Matches any number
router.get('/data/:val=string', stringHandler); // Matches anything
```
**Matching order:**
- `123` → `int` (most specific)
- `123.45` → `float` (more specific)
- `"abc"` → `string` (fallback)
---
## All Parameter Types
VeloxAPI supports **12 parameter types**:
| Type | Validates | Converts To | Example |
|------|-----------|-------------|---------|
| `string` | Any string | String | `"abc"`, `"123"` |
| `number` | Any number | Number | `123`, `123.45` |
| `int` | Integer only | Number | `123` |
| `float` | Float only | Number | `123.45` |
| `boolean` | true/false | Boolean | `true`, `false` |
| `email` | Email format | String | `user@example.com` |
| `url` | URL format | String | `https://example.com` |
| `uuid` | UUID format | String | `550e8400-e29b-41d4-a716-446655440000` |
| `slug` | Slug format | String | `my-blog-post` |
| `alpha` | Letters only | String | `abc`, `ABC` |
| `alphanumeric` | Letters + numbers | String | `abc123` |
| `hex` | Hexadecimal | String | `FF5733` |
### Validation Rules
#### `string`
- Accepts: Any string
- Returns: String
#### `number`
- Accepts: Any valid number (int or float)
- Returns: Number
- Examples: `123`, `123.45`, `-99`, `0.001`
#### `int`
- Accepts: Integers only (no decimals)
- Returns: Number (integer)
- Examples: `123`, `-45`, `0`
- Rejects: `123.45`
#### `float`
- Accepts: Decimal numbers only
- Returns: Number (float)
- Examples: `123.45`, `-99.99`, `0.001`
- Rejects: `123` (use `int` for integers)
#### `boolean`
- Accepts: `"true"`, `"false"`, `"1"`, `"0"`
- Returns: Boolean
- Examples: `true`, `false`
#### `email`
- Accepts: Valid email format
- Returns: String
- Examples: `user@example.com`, `test+tag@domain.co.uk`
- Rejects: `not-an-email`
#### `url`
- Accepts: Valid URL format
- Returns: String
- Examples: `https://example.com`, `http://localhost:3000`
- Rejects: `not-a-url`
#### `uuid`
- Accepts: UUID v4 format
- Returns: String
- Example: `550e8400-e29b-41d4-a716-446655440000`
- Rejects: `not-a-uuid`
#### `slug`
- Accepts: Lowercase letters, numbers, hyphens
- Returns: String
- Examples: `my-post`, `blog-2024`, `hello-world`
- Rejects: `My Post` (has spaces)
#### `alpha`
- Accepts: Letters only (a-z, A-Z)
- Returns: String
- Examples: `abc`, `ABC`, `Hello`
- Rejects: `abc123` (has numbers)
#### `alphanumeric`
- Accepts: Letters and numbers only
- Returns: String
- Examples: `abc123`, `User123`, `test`
- Rejects: `hello-world` (has hyphen)
#### `hex`
- Accepts: Hexadecimal characters (0-9, A-F)
- Returns: String
- Examples: `FF5733`, `000000`, `FFFFFF`
- Rejects: `GGGGGG` (invalid hex)
---
## Advanced Patterns
### Multiple Parameters with Different Types
Combine multiple typed parameters in a single route:
```javascript
router.get('/api/:version=int/users/:userId=uuid', (res, req, query, params) => {
res.sendJSON({
version: params.version, // number
userId: params.userId // string (UUID)
});
});
```
**Matches:**
- `/api/1/users/550e8400-e29b-41d4-a716-446655440000` ✅
**Rejects:**
- `/api/1.5/users/...` ❌ (version not int)
- `/api/1/users/not-a-uuid` ❌ (userId not UUID)
### Variation at Different Levels
Create route variations at any nesting level:
```javascript
// Version 1 with number IDs
router.get('/api/:v=int/items/:id=number', handler1);
// Version 1 with string IDs
router.get('/api/:v=int/items/:id=string', handler2);
// Version 2 with UUID IDs
router.get('/api/:v=float/items/:id=uuid', handler3);
```
**Request routing:**
- `/api/1/items/123` → handler1 (int + number)
- `/api/1/items/abc` → handler2 (int + string)
- `/api/1.5/items/550e8400-...` → handler3 (float + uuid)
### Email vs String Fallback
Handle validated emails separately from generic strings:
```javascript
router.get('/verify/:input=email', (res, req, query, params) => {
// Send verification email
sendVerificationEmail(params.input);
res.sendJSON({ message: 'Email sent' });
});
router.get('/verify/:input=string', (res, req, query, params) => {
// Handle non-email input
res.sendJSON({ error: 'Invalid email format' });
});
```
### Complex Nested Routing
Build deep nested routes with type validation:
```javascript
router.get(
'/orgs/:orgId=uuid/projects/:projectId=int/files/:fileId=string',
(res, req, query, params) => {
res.sendJSON({
org: params.orgId, // UUID string
project: params.projectId, // Integer
file: params.fileId // Any string
});
}
);
```
---
## Best Practices
### 1. Order Routes by Specificity
Define more specific types before generic ones:
```javascript
// ✅ Good: Specific to generic
router.get('/items/:id=uuid', uuidHandler);
router.get('/items/:id=int', intHandler);
router.get('/items/:id=string', stringHandler);
// ❌ Bad: Generic first (string catches everything)
router.get('/items/:id=string', stringHandler);
router.get('/items/:id=uuid', uuidHandler); // Never reached!
```
### 2. Use Type Validation for Security
Typed parameters prevent injection attacks:
```javascript
// ✅ Good: Type validated
router.get('/users/:id=int', (res, req, query, params) => {
// params.id is guaranteed to be a number
const user = db.getUserById(params.id); // Safe
});
// ❌ Bad: No validation
router.get('/users/:id', (res, req, query, params) => {
// params.id could be anything!
const user = db.getUserById(params.id); // SQL injection risk
});
```
### 3. Combine with Middleware
Use typed routing with middleware for powerful patterns:
```javascript
// Only admin users can access numeric IDs
router.get('/users/:id=number', requireAdmin, (res, req, query, params) => {
res.sendJSON({ admin: true, userId: params.id });
});
// Public access to username lookups
router.get('/users/:id=string', (res, req, query, params) => {
res.sendJSON({ public: true, username: params.id });
});
```
### 4. Document Your Routes
Clearly document which types are expected:
```javascript
/**
* Get user by ID
*
* Numeric ID (admin only):
* GET /users/123
*
* Username (public):
* GET /users/alice
*
* UUID (API integration):
* GET /users/550e8400-e29b-41d4-a716-446655440000
*/
router.get('/users/:id=number', requireAdmin, numericUserHandler);
router.get('/users/:id=string', publicUserHandler);
router.get('/users/:id=uuid', apiUserHandler);
```
### 5. Handle Edge Cases
Consider how overlapping types behave:
```javascript
// "123" is valid as both int and string
// Which route should handle it?
// Option 1: Let int handle it
router.get('/data/:val=int', intHandler);
router.get('/data/:val=string', stringHandler);
// Option 2: Let string handle it
router.get('/data/:val=string', stringHandler);
router.get('/data/:val=int', intHandler); // Never reached for "123"
// Option 3: Be explicit with both
router.get('/data/:val=int', (res, req, query, params) => {
res.sendJSON({ type: 'int', value: params.val });
});
router.get('/data/:val=string', (res, req, query, params) => {
res.sendJSON({ type: 'string', value: params.val });
});
```
---
## Performance
### Radix Tree Optimization
VeloxAPI uses a **radix tree** with type-aware branching:
**Traditional router (O(n) linear):**
```
Check route 1 → No match
Check route 2 → No match
Check route 3 → Match! (slow for many routes)
```
**VeloxAPI (O(log n) logarithmic):**
```
/users
├── :id=number → Direct lookup
└── :id=string → Direct lookup
```
### Benchmarks
Multiple typed routes add **minimal overhead**:
```javascript
// 1000 routes with different types
for (let i = 0; i < 250; i++) {
router.get(`/route${i}/:id=number`, handler);
router.get(`/route${i}/:id=string`, handler);
router.get(`/route${i}/:id=uuid`, handler);
router.get(`/route${i}/:id=slug`, handler);
}
// Lookup time: ~0.001ms (same as single route!)
```
### Memory Usage
Each unique path+type combination creates one tree node:
```javascript
// Memory usage
router.get('/users/:id=number', h1); // +1 node
router.get('/users/:id=string', h2); // +1 node
router.get('/posts/:id=uuid', h3); // +2 nodes (posts + :id=uuid)
// Total: 4 nodes, ~200 bytes
```
---
## Testing Typed Routes
### Unit Tests
```javascript
import { describe, test, expect } from '@jest/globals';
import { RadixTree } from 'veloxapi/lib/utils/radix-tree';
test('should route by type', () => {
const tree = new RadixTree();
tree.insert('/items/:id=number', numHandler);
tree.insert('/items/:id=string', strHandler);
const numResult = tree.search('/items/123');
expect(numResult.handler).toBe(numHandler);
expect(numResult.params.id).toBe(123);
const strResult = tree.search('/items/abc');
expect(strResult.handler).toBe(strHandler);
expect(strResult.params.id).toBe('abc');
});
```
### Integration Tests
```javascript
test('should handle typed routes via HTTP', async () => {
const response = await fetch('http://localhost:3000/users/123');
const data = await response.json();
expect(data.type).toBe('number');
expect(typeof data.id).toBe('number');
});
```
---
## Complete Example
Here's a production-ready API using advanced typed routing:
```javascript
import { VeloxServer, VeloxRouter } from 'veloxapi';
const router = new VeloxRouter();
// User routes with different ID types
router.get('/users/:id=int', (res, req, query, params) => {
// Internal numeric IDs (admin/backend)
const user = db.users.findById(params.id);
res.sendJSON({ user });
});
router.get('/users/:id=uuid', (res, req, query, params) => {
// External UUID (API integration)
const user = db.users.findByUuid(params.id);
res.sendJSON({ user });
});
router.get('/users/:id=slug', (res, req, query, params) => {
// Username/slug (public profile)
const user = db.users.findByUsername(params.id);
res.sendJSON({ user });
});
// Product routes with price types
router.get('/products/:price=int', (res, req, query, params) => {
// Whole dollar amounts
const products = db.products.findByExactPrice(params.price);
res.sendJSON({ products });
});
router.get('/products/:price=float', (res, req, query, params) => {
// Decimal prices
const products = db.products.findByPrice(params.price);
res.sendJSON({ products });
});
// API versioning with nested types
router.get('/api/:version=int/items/:id=number', (res, req, query, params) => {
res.sendJSON({
apiVersion: params.version,
item: { id: params.id }
});
});
router.get('/api/:version=float/items/:id=uuid', (res, req, query, params) => {
res.sendJSON({
apiVersion: params.version,
item: { uuid: params.id }
});
});
new VeloxServer().setPort(3000).setRouter(router).start();
```
---
## Migration Guide
### From Untyped to Typed Routes
**Before:**
```javascript
router.get('/users/:id', (res, req, query, params) => {
// Manual validation
const id = parseInt(params.id);
if (isNaN(id)) {
return res.sendError('Invalid ID', 400);
}
const user = db.getUserById(id);
res.sendJSON({ user });
});
```
**After:**
```javascript
router.get('/users/:id=int', (res, req, query, params) => {
// Automatic validation and conversion
const user = db.getUserById(params.id);
res.sendJSON({ user });
});
```
### Adding Multiple Type Support
**Before:**
```javascript
router.get('/posts/:id', (res, req, query, params) => {
// Handle both UUID and slug
if (isUuid(params.id)) {
return getByUuid(params.id);
}
return getBySlug(params.id);
});
```
**After:**
```javascript
router.get('/posts/:id=uuid', (res, req, query, params) => {
return getByUuid(params.id);
});
router.get('/posts/:id=slug', (res, req, query, params) => {
return getBySlug(params.id);
});
```
---
## FAQ
### Q: What happens if multiple types match?
A: The **first valid route** wins. Define specific types before generic ones.
### Q: Can I have more than 2 types for the same parameter?
A: Yes! You can define as many type variations as needed:
```javascript
router.get('/data/:val=int', h1);
router.get('/data/:val=float', h2);
router.get('/data/:val=email', h3);
router.get('/data/:val=uuid', h4);
router.get('/data/:val=string', h5); // Fallback
```
### Q: Does this impact performance?
A: Minimal impact. The radix tree efficiently branches by type, maintaining O(log n) lookups.
### Q: Can I mix typed and untyped parameters?
A: Yes:
```javascript
router.get('/api/:version=int/items/:id', handler); // :version typed, :id untyped
```
---
## Next Steps
- **[API Reference](./API.md)** - Complete API documentation
- **[Typed Parameters Tutorial](../learn/02-typed-parameters.md)** - Learn the basics
- **[Performance Guide](./PERFORMANCE.md)** - Optimization tips
---
**Made with ❤️ for performance | VeloxAPI v0.2.0-alpha.1**