Express.js Integration

Learn how to integrate the Asterisms JS SDK Backend with Express.js applications.

Overview

The Asterisms JS SDK Backend provides seamless integration with Express.js applications through middleware, controllers, and utility functions. This guide covers common patterns and best practices for building robust Express.js applications with the SDK.

Basic Setup

Express App with SDK

// app.js
const express = require('express');
const { getAsterismsBackendSDK } = require('@asterisms/sdk-backend');
const { createFetchAdapter } = require('@asterisms/sdk-backend/adapters/fetch');

const app = express();

// Initialize SDK
let sdk = null;

async function initializeSDK() {
  sdk = getAsterismsBackendSDK({
    bundleId: 'com.example.express-app',
    domain: 'asterisms.example.com',
    subdomain: 'express-app',
    httpServiceAdapterFactory: createFetchAdapter(fetch),
    registrationData: {
      bundleId: 'com.example.express-app',
      capabilities: ['BACKEND_SERVICE', 'WEBHOOKS'],
      description: 'Express.js Application with Asterisms JS SDK',
      name: 'Express App',
      subdomain: 'express-app',
      title: 'Express App'
    }
  });

  await sdk.boot();
  console.log('SDK initialized successfully');
}

// Middleware to make SDK available in all routes
app.use((req, res, next) => {
  req.sdk = sdk;
  next();
});

// Basic middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    sdkBooted: req.sdk ? req.sdk.isBooted() : false
  });
});

// Start server
const PORT = process.env.PORT || 3000;

initializeSDK()
  .then(() => {
    app.listen(PORT, () => {
      console.log(`Server running on port ${PORT}`);
    });
  })
  .catch((error) => {
    console.error('Failed to initialize SDK:', error);
    process.exit(1);
  });

module.exports = app;

TypeScript Setup

// app.ts
import express from 'express';
import { getAsterismsBackendSDK } from '@asterisms/sdk-backend';
import { createFetchAdapter } from '@asterisms/sdk-backend/adapters/fetch';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';

// Extend Express Request type
declare global {
  namespace Express {
    interface Request {
      sdk: AsterismsBackendSDK;
    }
  }
}

const app = express();
let sdk: AsterismsBackendSDK;

async function initializeSDK(): Promise<void> {
  sdk = getAsterismsBackendSDK({
    bundleId: 'com.example.express-app',
    domain: 'asterisms.example.com',
    subdomain: 'express-app',
    httpServiceAdapterFactory: createFetchAdapter(fetch),
    registrationData: {
      bundleId: 'com.example.express-app',
      capabilities: ['BACKEND_SERVICE', 'WEBHOOKS'],
      description: 'Express.js Application with Asterisms JS SDK',
      name: 'Express App',
      subdomain: 'express-app',
      title: 'Express App'
    }
  });

  await sdk.boot();
  console.log('SDK initialized successfully');
}

// Middleware to make SDK available in all routes
app.use((req, res, next) => {
  req.sdk = sdk;
  next();
});

// Basic middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

export { app, initializeSDK };

Authentication Middleware

Basic Authentication

// middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
import { AsterismsSDKError } from '@asterisms/sdk-backend';

export const authMiddleware = async (
  req: Request,
  res: Response,
  next: NextFunction
): Promise<void> => {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      res.status(401).json({ error: 'No token provided' });
      return;
    }

    const token = authHeader.substring(7);
    const auth = req.sdk.authorization();

    // Validate token and get user
    const actor = await auth.resolveAuthenticatedActor(token);
    req.user = actor;

    next();
  } catch (error) {
    if (error instanceof AsterismsSDKError) {
      res.status(401).json({ error: 'Invalid token' });
    } else {
      res.status(500).json({ error: 'Authentication failed' });
    }
  }
};

Permission-Based Middleware

// middleware/permissions.ts
import { Request, Response, NextFunction } from 'express';

export const requirePermission = (permission: string) => {
  return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const auth = req.sdk.authorization();

      // Check if user has the required permission
      // Note: This is a simplified example - actual permission checking
      // would depend on your authorization system implementation
      if (!req.user || !req.user.account) {
        res.status(403).json({
          error: `Missing permission: ${permission}`
        });
        return;
      }

      // You would implement actual permission checking here
      // based on your authorization system
      next();
    } catch (error) {
      res.status(500).json({ error: 'Permission check failed' });
    }
  };
};

// Usage
app.get('/admin/users', authMiddleware, requirePermission('admin.users.read'), (req, res) => {
  // Admin route logic
});

CRUD Operations

User Management Routes

// routes/users.ts
import express from 'express';
import { authMiddleware, requirePermission } from '../middleware';

const router = express.Router();

// Get all users
router.get('/', authMiddleware, requirePermission('users.read'), async (req, res) => {
  try {
    const storage = req.sdk.storage();
    const users = await storage.list({ path: '/users' });
    res.json({ users });
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch users' });
  }
});

// Get user by ID
router.get('/:id', authMiddleware, requirePermission('users.read'), async (req, res) => {
  try {
    const storage = req.sdk.storage();
    const user = await storage.getData(req.params.id);

    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    res.json({ user });
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch user' });
  }
});

// Create new user
router.post('/', authMiddleware, requirePermission('users.create'), async (req, res) => {
  try {
    const { email, name, role } = req.body;

    // Validate input
    if (!email || !name) {
      return res.status(400).json({ error: 'Email and name are required' });
    }

    const storage = req.sdk.storage();

    // Check if user already exists
    const existingUsers = await storage.list({ path: '/users' });
    const existingUser = existingUsers.find((u) => u.email === email);
    if (existingUser) {
      return res.status(409).json({ error: 'User already exists' });
    }

    // Create user by uploading user data as a file
    const userData = {
      email,
      name,
      role: role || 'user',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };

    const userDataBuffer = Buffer.from(JSON.stringify(userData));
    const wrappedFile = {
      buffer: userDataBuffer,
      type: 'application/json',
      name: `user-${email}.json`,
      size: userDataBuffer.length
    };

    const newUser = await storage.upload(wrappedFile, {
      prefix: 'users',
      metadata: { type: 'user' }
    });

    res.status(201).json({ user: newUser });
  } catch (error) {
    res.status(500).json({ error: 'Failed to create user' });
  }
});

// Update user
router.put('/:id', authMiddleware, requirePermission('users.update'), async (req, res) => {
  try {
    const { name, role } = req.body;
    const storage = req.sdk.storage();

    // Check if user exists
    const existingUser = await storage.getData(req.params.id);
    if (!existingUser) {
      return res.status(404).json({ error: 'User not found' });
    }

    // Update user data
    const updatedUserData = {
      ...existingUser,
      name,
      role,
      updatedAt: new Date().toISOString()
    };

    const userDataBuffer = Buffer.from(JSON.stringify(updatedUserData));
    const wrappedFile = {
      buffer: userDataBuffer,
      type: 'application/json',
      name: `user-${req.params.id}.json`,
      size: userDataBuffer.length
    };

    const updatedUser = await storage.replace(req.params.id, wrappedFile, {
      metadata: { type: 'user' }
    });

    res.json({ user: updatedUser });
  } catch (error) {
    res.status(500).json({ error: 'Failed to update user' });
  }
});

// Delete user
router.delete('/:id', authMiddleware, requirePermission('users.delete'), async (req, res) => {
  try {
    const storage = req.sdk.storage();

    // Check if user exists
    const existingUser = await storage.getData(req.params.id);
    if (!existingUser) {
      return res.status(404).json({ error: 'User not found' });
    }

    await storage.delete(req.params.id);
    res.json({ success: true });
  } catch (error) {
    res.status(500).json({ error: 'Failed to delete user' });
  }
});

export default router;

Using the Routes

// app.ts
import userRoutes from './routes/users';

app.use('/api/users', userRoutes);

File Upload Example

File Upload Route

// routes/files.ts
import express from 'express';
import multer from 'multer';
import { authMiddleware, requirePermission } from '../middleware';

const router = express.Router();

// Configure multer for file uploads
const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024 // 10MB limit
  },
  fileFilter: (req, file, cb) => {
    const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
    if (allowedTypes.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Invalid file type'));
    }
  }
});

// Upload file
router.post(
  '/upload',
  authMiddleware,
  requirePermission('files.upload'),
  upload.single('file'),
  async (req, res) => {
    try {
      if (!req.file) {
        return res.status(400).json({ error: 'No file provided' });
      }

      const storage = req.sdk.storage();

      // Store file
      const wrappedFile = {
        buffer: req.file.buffer,
        type: req.file.mimetype,
        name: req.file.originalname,
        size: req.file.size
      };

      const uploadResult = await storage.upload(wrappedFile, {
        prefix: 'uploads',
        metadata: {
          uploadedBy: req.user.id,
          uploadedAt: new Date().toISOString()
        }
      });

      // Store file metadata
      const fileMetadata = {
        key: uploadResult.externalId,
        originalName: req.file.originalname,
        mimeType: req.file.mimetype,
        size: req.file.size,
        uploadedBy: req.user.id,
        uploadedAt: new Date().toISOString()
      };

      const metadataBuffer = Buffer.from(JSON.stringify(fileMetadata));
      const metadataFile = {
        buffer: metadataBuffer,
        type: 'application/json',
        name: `metadata-${uploadResult.externalId}.json`,
        size: metadataBuffer.length
      };

      const savedMetadata = await storage.upload(metadataFile, {
        prefix: 'file_metadata',
        metadata: { type: 'file_metadata' }
      });

      res.status(201).json({
        file: {
          id: savedMetadata.id,
          url: `/api/files/${savedMetadata.id}`,
          ...fileMetadata
        }
      });
    } catch (error) {
      res.status(500).json({ error: 'Failed to upload file' });
    }
  }
);

// Get file
router.get('/:id', async (req, res) => {
  try {
    const storage = req.sdk.storage();

    // Get file metadata
    const metadata = await storage.getData(req.params.id);
    if (!metadata) {
      return res.status(404).json({ error: 'File not found' });
    }

    // Get file content
    const fileResponse = await storage.getFile(req.params.id);
    if (!fileResponse) {
      return res.status(404).json({ error: 'File content not found' });
    }

    // Stream the file response
    const buffer = await fileResponse.arrayBuffer();

    res.set({
      'Content-Type': metadata.contentType,
      'Content-Length': metadata.size.toString(),
      'Content-Disposition': `attachment; filename="${metadata.fileName}"`
    });

    res.send(Buffer.from(buffer));
  } catch (error) {
    res.status(500).json({ error: 'Failed to retrieve file' });
  }
});

export default router;

WebSocket Integration

WebSocket Server

// websocket/server.ts
import WebSocket from 'ws';
import { IncomingMessage } from 'http';
import { AsterismsBackendSDK } from '@asterisms/sdk-backend';

interface WebSocketClient extends WebSocket {
  userId?: string;
  subscriptions: Set<string>;
}

export class WebSocketServer {
  private wss: WebSocket.Server;
  private clients = new Map<string, WebSocketClient>();

  constructor(
    private sdk: AsterismsBackendSDK,
    server: any
  ) {
    this.wss = new WebSocket.Server({ server });
    this.setupWebSocketServer();
  }

  private setupWebSocketServer(): void {
    this.wss.on('connection', (ws: WebSocketClient, req: IncomingMessage) => {
      const clientId = this.generateClientId();
      ws.subscriptions = new Set();
      this.clients.set(clientId, ws);

      ws.on('message', (message: string) => {
        this.handleMessage(clientId, ws, message);
      });

      ws.on('close', () => {
        this.clients.delete(clientId);
        console.log(`Client ${clientId} disconnected`);
      });

      ws.on('error', (error) => {
        console.error(`WebSocket error for client ${clientId}:`, error);
      });

      // Send welcome message
      ws.send(
        JSON.stringify({
          type: 'connection',
          clientId,
          message: 'Connected to WebSocket server'
        })
      );
    });
  }

  private async handleMessage(
    clientId: string,
    ws: WebSocketClient,
    message: string
  ): Promise<void> {
    try {
      const data = JSON.parse(message);

      switch (data.type) {
        case 'authenticate':
          await this.handleAuthentication(clientId, ws, data.token);
          break;
        case 'subscribe':
          await this.handleSubscription(clientId, ws, data.channel);
          break;
        case 'unsubscribe':
          await this.handleUnsubscription(clientId, ws, data.channel);
          break;
        case 'message':
          await this.handleChannelMessage(clientId, ws, data);
          break;
        default:
          ws.send(
            JSON.stringify({
              type: 'error',
              message: 'Unknown message type'
            })
          );
      }
    } catch (error) {
      ws.send(
        JSON.stringify({
          type: 'error',
          message: 'Invalid message format'
        })
      );
    }
  }

  private async handleAuthentication(
    clientId: string,
    ws: WebSocketClient,
    token: string
  ): Promise<void> {
    try {
      const auth = this.sdk.authorization();
      const actor = await auth.resolveAuthenticatedActor(token);

      ws.userId = actor.account.id;

      ws.send(
        JSON.stringify({
          type: 'authenticated',
          user: {
            id: actor.account.id,
            name: actor.account.name,
            email: actor.account.emailAddress
          }
        })
      );
    } catch (error) {
      ws.send(
        JSON.stringify({
          type: 'auth_error',
          message: 'Authentication failed'
        })
      );
    }
  }

  private async handleSubscription(
    clientId: string,
    ws: WebSocketClient,
    channel: string
  ): Promise<void> {
    if (!ws.userId) {
      ws.send(
        JSON.stringify({
          type: 'error',
          message: 'Authentication required'
        })
      );
      return;
    }

    ws.subscriptions.add(channel);

    ws.send(
      JSON.stringify({
        type: 'subscribed',
        channel
      })
    );
  }

  private async handleUnsubscription(
    clientId: string,
    ws: WebSocketClient,
    channel: string
  ): Promise<void> {
    ws.subscriptions.delete(channel);

    ws.send(
      JSON.stringify({
        type: 'unsubscribed',
        channel
      })
    );
  }

  private async handleChannelMessage(
    clientId: string,
    ws: WebSocketClient,
    data: any
  ): Promise<void> {
    if (!ws.userId) {
      ws.send(
        JSON.stringify({
          type: 'error',
          message: 'Authentication required'
        })
      );
      return;
    }

    const { channel, message } = data;

    // Broadcast to all clients subscribed to this channel
    this.broadcastToChannel(channel, {
      type: 'channel_message',
      channel,
      message,
      sender: ws.userId,
      timestamp: new Date().toISOString()
    });
  }

  public broadcastToChannel(channel: string, message: any): void {
    const messageString = JSON.stringify(message);

    this.clients.forEach((client) => {
      if (client.subscriptions.has(channel) && client.readyState === WebSocket.OPEN) {
        client.send(messageString);
      }
    });
  }

  private generateClientId(): string {
    return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

Integration with Express

// app.ts
import { createServer } from 'http';
import { WebSocketServer } from './websocket/server';

const server = createServer(app);
let wsServer: WebSocketServer;

initializeSDK()
  .then(() => {
    wsServer = new WebSocketServer(sdk, server);

    server.listen(PORT, () => {
      console.log(`Server running on port ${PORT}`);
      console.log(`WebSocket server ready`);
    });
  })
  .catch((error) => {
    console.error('Failed to initialize:', error);
    process.exit(1);
  });

Error Handling

Global Error Handler

// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { AsterismsSDKError } from '@asterisms/sdk-backend';

export const errorHandler = (
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
): void => {
  console.error('Global error handler:', error);

  if (error instanceof AsterismsSDKError) {
    res.status(500).json({
      error: 'SDK Error',
      message: error.message,
      code: error.code
    });
  } else if (error.name === 'ValidationError') {
    res.status(400).json({
      error: 'Validation Error',
      message: error.message
    });
  } else {
    res.status(500).json({
      error: 'Internal Server Error',
      message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'
    });
  }
};

// Usage
app.use(errorHandler);

Environment Configuration

Configuration Management

// config/index.ts
import { config } from 'dotenv';
config();

export const appConfig = {
  port: parseInt(process.env.PORT || '3000'),
  nodeEnv: process.env.NODE_ENV || 'development',
  sdk: {
    bundleId: process.env.BUNDLE_ID || 'com.example.express-app',
    domain: process.env.ASTERISMS_DOMAIN || 'asterisms.example.com',
    subdomain: process.env.SUBDOMAIN || 'express-app',
    secure: process.env.SECURE === 'true',
    productAuthorizationKey: process.env.PRODUCT_AUTH_KEY
  },
  cors: {
    origin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'],
    credentials: true
  }
};

Testing

Unit Tests

// tests/routes/users.test.ts
import request from 'supertest';
import { app } from '../../app';

describe('User Routes', () => {
  let authToken: string;

  beforeAll(async () => {
    // Get auth token for tests
    const loginResponse = await request(app).post('/api/auth/login').send({
      email: 'test@example.com',
      password: 'password'
    });

    authToken = loginResponse.body.token;
  });

  describe('GET /api/users', () => {
    it('should return users list', async () => {
      const response = await request(app)
        .get('/api/users')
        .set('Authorization', `Bearer ${authToken}`);

      expect(response.status).toBe(200);
      expect(response.body.users).toBeDefined();
    });

    it('should return 401 without token', async () => {
      const response = await request(app).get('/api/users');

      expect(response.status).toBe(401);
    });
  });

  describe('POST /api/users', () => {
    it('should create new user', async () => {
      const userData = {
        email: 'newuser@example.com',
        name: 'New User',
        role: 'user'
      };

      const response = await request(app)
        .post('/api/users')
        .set('Authorization', `Bearer ${authToken}`)
        .send(userData);

      expect(response.status).toBe(201);
      expect(response.body.user.email).toBe(userData.email);
    });
  });
});

Production Considerations

Process Management

// server.ts
import { app, initializeSDK } from './app';
import { appConfig } from './config';

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1);
});

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  process.exit(1);
});

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully...');
  process.exit(0);
});

process.on('SIGINT', () => {
  console.log('SIGINT received, shutting down gracefully...');
  process.exit(0);
});

initializeSDK()
  .then(() => {
    app.listen(appConfig.port, () => {
      console.log(`Server running on port ${appConfig.port}`);
    });
  })
  .catch((error) => {
    console.error('Failed to initialize SDK:', error);
    process.exit(1);
  });

Health Check Endpoint

// routes/health.ts
import express from 'express';

const router = express.Router();

router.get('/', async (req, res) => {
  try {
    const health = {
      status: 'healthy',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
      sdk: {
        booted: req.sdk.isBooted()
      },
      memory: process.memoryUsage(),
      version: process.env.npm_package_version
    };

    res.json(health);
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message
    });
  }
});

export default router;

Best Practices

Security

// Security middleware
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';

app.use(helmet());
app.use(cors(appConfig.cors));

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests from this IP'
});

app.use('/api/', limiter);

Logging

// middleware/logging.ts
import morgan from 'morgan';

export const loggingMiddleware = morgan('combined', {
  stream: {
    write: (message) => {
      const logger = global.sdk?.logging();
      if (logger) {
        logger.info(message.trim());
      } else {
        console.log(message.trim());
      }
    }
  }
});

app.use(loggingMiddleware);

Next Steps