|
| 1 | +# Custom API Routes Configuration |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +ObjectQL allows you to configure custom API route paths during initialization instead of using hardcoded default paths. This feature provides flexibility for: |
| 6 | + |
| 7 | +- **API Versioning**: Use paths like `/v1/api`, `/v2/api` |
| 8 | +- **Custom Naming**: Use domain-specific naming like `/resources`, `/schema` |
| 9 | +- **Multiple API Instances**: Run multiple ObjectQL instances with different paths |
| 10 | +- **Integration Requirements**: Align with existing API structures |
| 11 | + |
| 12 | +## Default Routes |
| 13 | + |
| 14 | +By default, ObjectQL uses these API paths: |
| 15 | + |
| 16 | +| Endpoint Type | Default Path | Description | |
| 17 | +|--------------|--------------|-------------| |
| 18 | +| JSON-RPC | `/api/objectql` | Remote procedure calls | |
| 19 | +| REST Data API | `/api/data` | CRUD operations on objects | |
| 20 | +| Metadata API | `/api/metadata` | Schema and metadata information | |
| 21 | +| File Operations | `/api/files` | File upload and download | |
| 22 | + |
| 23 | +## Configuration |
| 24 | + |
| 25 | +### Basic Usage |
| 26 | + |
| 27 | +Configure custom routes when creating handlers: |
| 28 | + |
| 29 | +```typescript |
| 30 | +import { createNodeHandler, createRESTHandler, createMetadataHandler } from '@objectql/server'; |
| 31 | + |
| 32 | +// Define custom routes |
| 33 | +const customRoutes = { |
| 34 | + rpc: '/v1/rpc', |
| 35 | + data: '/v1/resources', |
| 36 | + metadata: '/v1/schema', |
| 37 | + files: '/v1/storage' |
| 38 | +}; |
| 39 | + |
| 40 | +// Create handlers with custom routes |
| 41 | +const nodeHandler = createNodeHandler(app, { routes: customRoutes }); |
| 42 | +const restHandler = createRESTHandler(app, { routes: customRoutes }); |
| 43 | +const metadataHandler = createMetadataHandler(app, { routes: customRoutes }); |
| 44 | +``` |
| 45 | + |
| 46 | +### Route Configuration Interface |
| 47 | + |
| 48 | +```typescript |
| 49 | +interface ApiRouteConfig { |
| 50 | + /** |
| 51 | + * Base path for JSON-RPC endpoint |
| 52 | + * @default '/api/objectql' |
| 53 | + */ |
| 54 | + rpc?: string; |
| 55 | + |
| 56 | + /** |
| 57 | + * Base path for REST data API |
| 58 | + * @default '/api/data' |
| 59 | + */ |
| 60 | + data?: string; |
| 61 | + |
| 62 | + /** |
| 63 | + * Base path for metadata API |
| 64 | + * @default '/api/metadata' |
| 65 | + */ |
| 66 | + metadata?: string; |
| 67 | + |
| 68 | + /** |
| 69 | + * Base path for file operations |
| 70 | + * @default '/api/files' |
| 71 | + */ |
| 72 | + files?: string; |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +## Complete Example |
| 77 | + |
| 78 | +```typescript |
| 79 | +import express from 'express'; |
| 80 | +import { ObjectQL } from '@objectql/core'; |
| 81 | +import { SqlDriver } from '@objectql/driver-sql'; |
| 82 | +import { createNodeHandler, createRESTHandler, createMetadataHandler } from '@objectql/server'; |
| 83 | + |
| 84 | +async function main() { |
| 85 | + // 1. Initialize ObjectQL |
| 86 | + const app = new ObjectQL({ |
| 87 | + datasources: { |
| 88 | + default: new SqlDriver({ |
| 89 | + client: 'sqlite3', |
| 90 | + connection: { filename: ':memory:' }, |
| 91 | + useNullAsDefault: true |
| 92 | + }) |
| 93 | + } |
| 94 | + }); |
| 95 | + |
| 96 | + // Register your objects |
| 97 | + app.registerObject({ |
| 98 | + name: 'user', |
| 99 | + label: 'User', |
| 100 | + fields: { |
| 101 | + name: { type: 'text', label: 'Name' }, |
| 102 | + email: { type: 'email', label: 'Email' } |
| 103 | + } |
| 104 | + }); |
| 105 | + |
| 106 | + await app.init(); |
| 107 | + |
| 108 | + // 2. Define custom API routes |
| 109 | + const customRoutes = { |
| 110 | + rpc: '/v1/rpc', |
| 111 | + data: '/v1/resources', |
| 112 | + metadata: '/v1/schema', |
| 113 | + files: '/v1/storage' |
| 114 | + }; |
| 115 | + |
| 116 | + // 3. Create handlers with custom routes |
| 117 | + const nodeHandler = createNodeHandler(app, { routes: customRoutes }); |
| 118 | + const restHandler = createRESTHandler(app, { routes: customRoutes }); |
| 119 | + const metadataHandler = createMetadataHandler(app, { routes: customRoutes }); |
| 120 | + |
| 121 | + // 4. Setup Express with custom paths |
| 122 | + const server = express(); |
| 123 | + |
| 124 | + server.all('/v1/rpc*', nodeHandler); |
| 125 | + server.all('/v1/resources/*', restHandler); |
| 126 | + server.all('/v1/schema*', metadataHandler); |
| 127 | + |
| 128 | + server.listen(3000, () => { |
| 129 | + console.log('🚀 Server running with custom routes'); |
| 130 | + console.log(' JSON-RPC: http://localhost:3000/v1/rpc'); |
| 131 | + console.log(' REST API: http://localhost:3000/v1/resources'); |
| 132 | + console.log(' Metadata: http://localhost:3000/v1/schema'); |
| 133 | + console.log(' Files: http://localhost:3000/v1/storage'); |
| 134 | + }); |
| 135 | +} |
| 136 | + |
| 137 | +main().catch(console.error); |
| 138 | +``` |
| 139 | + |
| 140 | +## Using Custom Routes |
| 141 | + |
| 142 | +### JSON-RPC Endpoint |
| 143 | + |
| 144 | +**Default:** `POST /api/objectql` |
| 145 | +**Custom:** `POST /v1/rpc` |
| 146 | + |
| 147 | +```bash |
| 148 | +curl -X POST http://localhost:3000/v1/rpc \ |
| 149 | + -H "Content-Type: application/json" \ |
| 150 | + -d '{ |
| 151 | + "op": "find", |
| 152 | + "object": "user", |
| 153 | + "args": {} |
| 154 | + }' |
| 155 | +``` |
| 156 | + |
| 157 | +### REST Data API |
| 158 | + |
| 159 | +**Default:** `/api/data/:object` |
| 160 | +**Custom:** `/v1/resources/:object` |
| 161 | + |
| 162 | +```bash |
| 163 | +# List users |
| 164 | +curl http://localhost:3000/v1/resources/user |
| 165 | + |
| 166 | +# Get specific user |
| 167 | +curl http://localhost:3000/v1/resources/user/123 |
| 168 | + |
| 169 | +# Create user |
| 170 | +curl -X POST http://localhost:3000/v1/resources/user \ |
| 171 | + -H "Content-Type: application/json" \ |
| 172 | + -d '{"name": "Alice", "email": "alice@example.com"}' |
| 173 | + |
| 174 | +# Update user |
| 175 | +curl -X PUT http://localhost:3000/v1/resources/user/123 \ |
| 176 | + -H "Content-Type: application/json" \ |
| 177 | + -d '{"name": "Alice Updated"}' |
| 178 | + |
| 179 | +# Delete user |
| 180 | +curl -X DELETE http://localhost:3000/v1/resources/user/123 |
| 181 | +``` |
| 182 | + |
| 183 | +### Metadata API |
| 184 | + |
| 185 | +**Default:** `/api/metadata` |
| 186 | +**Custom:** `/v1/schema` |
| 187 | + |
| 188 | +```bash |
| 189 | +# List all objects |
| 190 | +curl http://localhost:3000/v1/schema/objects |
| 191 | + |
| 192 | +# Get object details |
| 193 | +curl http://localhost:3000/v1/schema/object/user |
| 194 | + |
| 195 | +# Get field metadata |
| 196 | +curl http://localhost:3000/v1/schema/object/user/fields/email |
| 197 | + |
| 198 | +# List object actions |
| 199 | +curl http://localhost:3000/v1/schema/object/user/actions |
| 200 | +``` |
| 201 | + |
| 202 | +### File Operations |
| 203 | + |
| 204 | +**Default:** `/api/files` |
| 205 | +**Custom:** `/v1/storage` |
| 206 | + |
| 207 | +```bash |
| 208 | +# Upload file |
| 209 | +curl -X POST http://localhost:3000/v1/storage/upload \ |
| 210 | + -F "file=@myfile.pdf" \ |
| 211 | + -F "object=document" \ |
| 212 | + -F "field=attachment" |
| 213 | + |
| 214 | +# Download file |
| 215 | +curl http://localhost:3000/v1/storage/abc123 |
| 216 | +``` |
| 217 | + |
| 218 | +## Client SDK Configuration |
| 219 | + |
| 220 | +The ObjectQL SDK clients also support custom route configuration: |
| 221 | + |
| 222 | +### Data API Client |
| 223 | + |
| 224 | +```typescript |
| 225 | +import { DataApiClient } from '@objectql/sdk'; |
| 226 | + |
| 227 | +const client = new DataApiClient({ |
| 228 | + baseUrl: 'http://localhost:3000', |
| 229 | + dataPath: '/v1/resources' // Custom data path |
| 230 | +}); |
| 231 | + |
| 232 | +const users = await client.list('user'); |
| 233 | +``` |
| 234 | + |
| 235 | +### Metadata API Client |
| 236 | + |
| 237 | +```typescript |
| 238 | +import { MetadataApiClient } from '@objectql/sdk'; |
| 239 | + |
| 240 | +const client = new MetadataApiClient({ |
| 241 | + baseUrl: 'http://localhost:3000', |
| 242 | + metadataPath: '/v1/schema' // Custom metadata path |
| 243 | +}); |
| 244 | + |
| 245 | +const objects = await client.listObjects(); |
| 246 | +``` |
| 247 | + |
| 248 | +### Remote Driver |
| 249 | + |
| 250 | +```typescript |
| 251 | +import { RemoteDriver } from '@objectql/sdk'; |
| 252 | + |
| 253 | +const driver = new RemoteDriver( |
| 254 | + 'http://localhost:3000', |
| 255 | + '/v1/rpc' // Custom RPC path |
| 256 | +); |
| 257 | +``` |
| 258 | + |
| 259 | +## Common Use Cases |
| 260 | + |
| 261 | +### API Versioning |
| 262 | + |
| 263 | +Support multiple API versions simultaneously: |
| 264 | + |
| 265 | +```typescript |
| 266 | +// API v1 |
| 267 | +const v1Routes = { |
| 268 | + rpc: '/api/v1/rpc', |
| 269 | + data: '/api/v1/data', |
| 270 | + metadata: '/api/v1/metadata', |
| 271 | + files: '/api/v1/files' |
| 272 | +}; |
| 273 | + |
| 274 | +// API v2 |
| 275 | +const v2Routes = { |
| 276 | + rpc: '/api/v2/rpc', |
| 277 | + data: '/api/v2/data', |
| 278 | + metadata: '/api/v2/metadata', |
| 279 | + files: '/api/v2/files' |
| 280 | +}; |
| 281 | + |
| 282 | +const v1Handler = createNodeHandler(appV1, { routes: v1Routes }); |
| 283 | +const v2Handler = createNodeHandler(appV2, { routes: v2Routes }); |
| 284 | + |
| 285 | +server.all('/api/v1/*', v1Handler); |
| 286 | +server.all('/api/v2/*', v2Handler); |
| 287 | +``` |
| 288 | + |
| 289 | +### Domain-Specific Naming |
| 290 | + |
| 291 | +Use business-friendly terminology: |
| 292 | + |
| 293 | +```typescript |
| 294 | +const businessRoutes = { |
| 295 | + rpc: '/business/operations', |
| 296 | + data: '/business/entities', |
| 297 | + metadata: '/business/definitions', |
| 298 | + files: '/business/documents' |
| 299 | +}; |
| 300 | +``` |
| 301 | + |
| 302 | +### Multi-Tenant Applications |
| 303 | + |
| 304 | +Isolate APIs per tenant: |
| 305 | + |
| 306 | +```typescript |
| 307 | +app.use('/:tenantId/api/*', (req, res, next) => { |
| 308 | + const tenantRoutes = { |
| 309 | + rpc: `/${req.params.tenantId}/api/rpc`, |
| 310 | + data: `/${req.params.tenantId}/api/data`, |
| 311 | + metadata: `/${req.params.tenantId}/api/metadata`, |
| 312 | + files: `/${req.params.tenantId}/api/files` |
| 313 | + }; |
| 314 | + |
| 315 | + const handler = createNodeHandler( |
| 316 | + getTenantApp(req.params.tenantId), |
| 317 | + { routes: tenantRoutes } |
| 318 | + ); |
| 319 | + |
| 320 | + handler(req, res); |
| 321 | +}); |
| 322 | +``` |
| 323 | + |
| 324 | +## Backward Compatibility |
| 325 | + |
| 326 | +All handlers maintain backward compatibility: |
| 327 | + |
| 328 | +- If no `routes` option is provided, default paths are used |
| 329 | +- Existing applications continue to work without changes |
| 330 | +- Migration to custom routes is opt-in |
| 331 | + |
| 332 | +```typescript |
| 333 | +// This still works with default routes |
| 334 | +const handler = createNodeHandler(app); |
| 335 | +// Uses /api/objectql, /api/data, /api/metadata, /api/files |
| 336 | +``` |
| 337 | + |
| 338 | +## Best Practices |
| 339 | + |
| 340 | +1. **Consistency**: Use the same route structure across all handlers |
| 341 | +2. **Documentation**: Document your custom routes for API consumers |
| 342 | +3. **Versioning**: Consider using versioned paths for production APIs |
| 343 | +4. **Testing**: Test custom routes thoroughly before deployment |
| 344 | +5. **Migration**: Plan gradual migration if changing existing routes |
| 345 | + |
| 346 | +## Related Documentation |
| 347 | + |
| 348 | +- [REST API Reference](./rest.md) |
| 349 | +- [JSON-RPC API Reference](./json-rpc.md) |
| 350 | +- [Metadata API Reference](./metadata.md) |
| 351 | +- [Client SDK Guide](./client-sdk.md) |
0 commit comments