API Configuration
This guide covers configuring and customizing the Veltix API for different use cases.
Overview
The Veltix API provides:
- RESTful endpoints for data operations
- WebSocket connections for real-time updates
- Authentication and authorization
- Rate limiting and security
- Custom middleware support
API Server Setup
Basic Configuration
// apps/api/src/server.ts
import fastify from 'fastify';
import cors from '@fastify/cors';
import rateLimit from '@fastify/rate-limit';
const server = fastify({
logger: true,
trustProxy: true
});
// CORS configuration
await server.register(cors, {
origin: ['http://localhost:3000', 'https://yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
});
// Rate limiting
await server.register(rateLimit, {
max: 100,
timeWindow: '1 minute',
allowList: ['127.0.0.1']
});
// Health check endpoint
server.get('/health', async (request, reply) => {
return { status: 'ok', timestamp: new Date().toISOString() };
});
// Start server
const start = async () => {
try {
await server.listen({ port: 3001, host: '0.0.0.0' });
} catch (err) {
server.log.error(err);
process.exit(1);
}
};
start();Environment Configuration
# .env
NODE_ENV=production
PORT=3001
HOST=0.0.0.0
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/veltix
# Redis
REDIS_URL=redis://localhost:6379
# JWT
JWT_SECRET=your-jwt-secret
JWT_EXPIRES_IN=7d
# API Keys
API_KEY_SECRET=your-api-key-secret
# CORS
CORS_ORIGIN=http://localhost:3000,https://yourdomain.com
# Rate Limiting
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=60000Authentication
JWT Authentication
// middleware/auth.ts
import jwt from 'jsonwebtoken';
import { FastifyRequest, FastifyReply } from 'fastify';
interface JWTPayload {
userId: string;
email: string;
role: string;
}
export const authenticateJWT = async (
request: FastifyRequest,
reply: FastifyReply
) => {
try {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
return reply.status(401).send({ error: 'No token provided' });
}
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
request.user = decoded;
} catch (error) {
return reply.status(401).send({ error: 'Invalid token' });
}
};
// Protected route
server.get('/api/dashboards', { preHandler: authenticateJWT }, async (request, reply) => {
const user = request.user as JWTPayload;
const dashboards = await getDashboardsByUserId(user.userId);
return { dashboards };
});API Key Authentication
// middleware/api-key.ts
import { FastifyRequest, FastifyReply } from 'fastify';
export const authenticateAPIKey = async (
request: FastifyRequest,
reply: FastifyReply
) => {
const apiKey = request.headers['x-api-key'] as string;
if (!apiKey) {
return reply.status(401).send({ error: 'API key required' });
}
// Validate API key
const isValid = await validateAPIKey(apiKey);
if (!isValid) {
return reply.status(401).send({ error: 'Invalid API key' });
}
request.apiKey = apiKey;
};
// API key protected route
server.get('/api/data', { preHandler: authenticateAPIKey }, async (request, reply) => {
const apiKey = request.apiKey as string;
const data = await getDataByAPIKey(apiKey);
return { data };
});Data Endpoints
Dashboard Endpoints
// routes/dashboards.ts
import { FastifyInstance } from 'fastify';
export default async function dashboardRoutes(fastify: FastifyInstance) {
// Get all dashboards
fastify.get('/api/dashboards', async (request, reply) => {
const dashboards = await fastify.prisma.dashboard.findMany({
where: { userId: request.user.userId }
});
return { dashboards };
});
// Get dashboard by ID
fastify.get('/api/dashboards/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const dashboard = await fastify.prisma.dashboard.findUnique({
where: { id },
include: { components: true }
});
if (!dashboard) {
return reply.status(404).send({ error: 'Dashboard not found' });
}
return { dashboard };
});
// Create dashboard
fastify.post('/api/dashboards', async (request, reply) => {
const { title, layout, components } = request.body as any;
const dashboard = await fastify.prisma.dashboard.create({
data: {
title,
layout,
userId: request.user.userId,
components: {
create: components
}
}
});
return { dashboard };
});
// Update dashboard
fastify.put('/api/dashboards/:id', async (request, reply) => {
const { id } = request.params as { id: string };
const updates = request.body as any;
const dashboard = await fastify.prisma.dashboard.update({
where: { id },
data: updates
});
return { dashboard };
});
// Delete dashboard
fastify.delete('/api/dashboards/:id', async (request, reply) => {
const { id } = request.params as { id: string };
await fastify.prisma.dashboard.delete({
where: { id }
});
return { success: true };
});
}Data Source Endpoints
// routes/data-sources.ts
export default async function dataSourceRoutes(fastify: FastifyInstance) {
// Get data from source
fastify.get('/api/data-sources/:id/data', async (request, reply) => {
const { id } = request.params as { id: string };
const { query } = request.query as { query?: string };
const dataSource = await fastify.prisma.dataSource.findUnique({
where: { id }
});
if (!dataSource) {
return reply.status(404).send({ error: 'Data source not found' });
}
const data = await fetchDataFromSource(dataSource, query);
return { data };
});
// Test data source connection
fastify.post('/api/data-sources/test', async (request, reply) => {
const config = request.body as any;
try {
const result = await testDataSourceConnection(config);
return { success: true, result };
} catch (error) {
return reply.status(400).send({
success: false,
error: error.message
});
}
});
}WebSocket Configuration
WebSocket Server
// websocket/server.ts
import { WebSocketServer } from 'ws';
import { createServer } from 'http';
const server = createServer();
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, request) => {
console.log('Client connected');
// Authenticate connection
const token = new URL(request.url!, `ws://${request.headers.host}`).searchParams.get('token');
if (!token) {
ws.close(1008, 'Authentication required');
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
ws.userId = decoded.userId;
} catch (error) {
ws.close(1008, 'Invalid token');
return;
}
// Handle messages
ws.on('message', (message) => {
try {
const data = JSON.parse(message.toString());
handleWebSocketMessage(ws, data);
} catch (error) {
ws.send(JSON.stringify({ error: 'Invalid message format' }));
}
});
// Handle disconnection
ws.on('close', () => {
console.log('Client disconnected');
});
});
server.listen(3002, () => {
console.log('WebSocket server running on port 3002');
});Real-time Updates
// websocket/handlers.ts
export const handleWebSocketMessage = (ws: WebSocket, data: any) => {
switch (data.type) {
case 'subscribe':
handleSubscription(ws, data);
break;
case 'unsubscribe':
handleUnsubscription(ws, data);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
break;
default:
ws.send(JSON.stringify({ error: 'Unknown message type' }));
}
};
const handleSubscription = (ws: WebSocket, data: any) => {
const { dashboardId, componentId } = data;
// Add to subscription list
if (!subscriptions.has(dashboardId)) {
subscriptions.set(dashboardId, new Set());
}
subscriptions.get(dashboardId)!.add(ws);
ws.send(JSON.stringify({
type: 'subscribed',
dashboardId,
componentId
}));
};
// Broadcast updates to subscribers
export const broadcastUpdate = (dashboardId: string, data: any) => {
const subscribers = subscriptions.get(dashboardId);
if (subscribers) {
const message = JSON.stringify({
type: 'update',
dashboardId,
data
});
subscribers.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
};Rate Limiting
Custom Rate Limiter
// middleware/rate-limiter.ts
import { FastifyRequest, FastifyReply } from 'fastify';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export const customRateLimiter = async (
request: FastifyRequest,
reply: FastifyReply
) => {
const key = `rate_limit:${request.ip}`;
const limit = parseInt(process.env.RATE_LIMIT_MAX || '100');
const window = parseInt(process.env.RATE_LIMIT_WINDOW || '60000');
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, window / 1000);
}
if (current > limit) {
return reply.status(429).send({
error: 'Too many requests',
retryAfter: Math.ceil(window / 1000)
});
}
reply.header('X-RateLimit-Limit', limit);
reply.header('X-RateLimit-Remaining', Math.max(0, limit - current));
reply.header('X-RateLimit-Reset', Date.now() + window);
};
// Apply to routes
server.addHook('preHandler', customRateLimiter);Error Handling
Global Error Handler
// middleware/error-handler.ts
import { FastifyError, FastifyReply } from 'fastify';
export const errorHandler = (
error: FastifyError,
request: FastifyRequest,
reply: FastifyReply
) => {
const statusCode = error.statusCode || 500;
const message = error.message || 'Internal Server Error';
// Log error
request.log.error(error);
// Send error response
reply.status(statusCode).send({
error: {
message,
code: error.code,
statusCode
}
});
};
// Register error handler
server.setErrorHandler(errorHandler);Validation Errors
// schemas/dashboard.ts
import { FastifySchema } from 'fastify';
export const createDashboardSchema: FastifySchema = {
body: {
type: 'object',
required: ['title'],
properties: {
title: { type: 'string', minLength: 1, maxLength: 100 },
layout: { type: 'object' },
components: { type: 'array' }
}
},
response: {
200: {
type: 'object',
properties: {
dashboard: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
layout: { type: 'object' },
createdAt: { type: 'string' }
}
}
}
}
}
};
// Use schema
server.post('/api/dashboards', {
schema: createDashboardSchema,
preHandler: authenticateJWT
}, async (request, reply) => {
// Handler implementation
});CORS Configuration
Advanced CORS Setup
// middleware/cors.ts
import cors from '@fastify/cors';
export const corsConfig = {
origin: (origin: string, callback: Function) => {
const allowedOrigins = process.env.CORS_ORIGIN?.split(',') || [
'http://localhost:3000'
];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-API-Key',
'X-Requested-With'
],
exposedHeaders: ['X-Total-Count', 'X-RateLimit-Remaining'],
maxAge: 86400 // 24 hours
};
// Register CORS
await server.register(cors, corsConfig);Logging
Structured Logging
// middleware/logging.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname'
}
}
});
// Request logging middleware
export const requestLogger = async (request: FastifyRequest, reply: FastifyReply) => {
const startTime = Date.now();
reply.addHook('onResponse', (request, reply, done) => {
const duration = Date.now() - startTime;
logger.info({
method: request.method,
url: request.url,
statusCode: reply.statusCode,
duration,
userAgent: request.headers['user-agent'],
ip: request.ip
});
done();
});
};Security
Security Headers
// middleware/security.ts
import helmet from '@fastify/helmet';
export const securityConfig = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "ws:", "wss:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
};
// Register security middleware
await server.register(helmet, securityConfig);Input Validation
// middleware/validation.ts
import { FastifyRequest, FastifyReply } from 'fastify';
export const validateInput = (schema: any) => {
return async (request: FastifyRequest, reply: FastifyReply) => {
try {
const { error } = schema.validate(request.body);
if (error) {
return reply.status(400).send({
error: 'Validation failed',
details: error.details
});
}
} catch (error) {
return reply.status(400).send({
error: 'Invalid input'
});
}
};
};Testing
API Testing
// tests/api.test.ts
import { test } from 'tap';
import { build } from '../src/app';
test('API endpoints', async (t) => {
const app = await build();
// Test health endpoint
const healthResponse = await app.inject({
method: 'GET',
url: '/health'
});
t.equal(healthResponse.statusCode, 200);
t.same(JSON.parse(healthResponse.payload), {
status: 'ok',
timestamp: healthResponse.payload.timestamp
});
// Test protected endpoint
const protectedResponse = await app.inject({
method: 'GET',
url: '/api/dashboards',
headers: {
authorization: 'Bearer invalid-token'
}
});
t.equal(protectedResponse.statusCode, 401);
});Configuration Examples
Production Configuration
// config/production.ts
export const productionConfig = {
server: {
port: process.env.PORT || 3001,
host: '0.0.0.0',
trustProxy: true
},
cors: {
origin: process.env.CORS_ORIGIN?.split(',') || [],
credentials: true
},
rateLimit: {
max: parseInt(process.env.RATE_LIMIT_MAX || '100'),
timeWindow: parseInt(process.env.RATE_LIMIT_WINDOW || '60000')
},
security: {
jwtSecret: process.env.JWT_SECRET!,
jwtExpiresIn: process.env.JWT_EXPIRES_IN || '7d'
}
};Development Configuration
// config/development.ts
export const developmentConfig = {
server: {
port: 3001,
host: 'localhost',
trustProxy: false
},
cors: {
origin: ['http://localhost:3000'],
credentials: true
},
rateLimit: {
max: 1000,
timeWindow: 60000
},
security: {
jwtSecret: 'dev-secret',
jwtExpiresIn: '30d'
}
};Best Practices
1. Security
- Use HTTPS in production
- Implement proper authentication
- Validate all inputs
- Use rate limiting
- Set security headers
2. Performance
- Use connection pooling
- Implement caching
- Optimize database queries
- Monitor response times
3. Reliability
- Implement proper error handling
- Use health checks
- Monitor API usage
- Set up logging
4. Maintainability
- Use TypeScript
- Write comprehensive tests
- Document API endpoints
- Follow consistent patterns
Last updated on