UNPKG

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
# 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**