Learn how to integrate the Asterisms JS SDK Backend with Express.js applications.
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.
// 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;// 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 };// 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' });
}
}
};// 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
});// 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;// app.ts
import userRoutes from './routes/users';
app.use('/api/users', userRoutes);// 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/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)}`;
}
}// 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);
});// 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);// 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
}
};// 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);
});
});
});// 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);
});// 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;// 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);// 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);