Learn how to organize business logic and request handling in SvelteKit applications with the Asterisms JS SDK Backend.
Controllers in SvelteKit are implemented as route handlers that process requests and return responses. The Asterisms JS SDK Backend provides patterns and utilities to organize your business logic effectively.
// src/routes/api/users/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ locals }) => {
const sdk = locals.sdk;
if (!sdk) {
return json({ error: 'SDK not available' }, { status: 503 });
}
try {
const storage = sdk.storage();
const users = await storage.list('users');
return json({ users });
} catch (error) {
console.error('Failed to fetch users:', error);
return json({ error: 'Failed to fetch users' }, { status: 500 });
}
};// src/lib/controllers/UserController.ts
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class UserController {
constructor(private sdk: AsterismsBackendSDK) {}
async getUsers(event: RequestEvent) {
try {
const storage = this.sdk.storage();
const users = await storage.list('users');
return json({ users });
} catch (error) {
console.error('Failed to fetch users:', error);
return json({ error: 'Failed to fetch users' }, { status: 500 });
}
}
async createUser(event: RequestEvent) {
try {
const userData = await event.request.json();
const storage = this.sdk.storage();
// Validate user data
this.validateUserData(userData);
const newUser = await storage.create('users', userData);
return json({ user: newUser }, { status: 201 });
} catch (error) {
console.error('Failed to create user:', error);
return json({ error: 'Failed to create user' }, { status: 500 });
}
}
async updateUser(event: RequestEvent) {
try {
const { params } = event;
const userData = await event.request.json();
const storage = this.sdk.storage();
const updatedUser = await storage.update('users', params.id, userData);
return json({ user: updatedUser });
} catch (error) {
console.error('Failed to update user:', error);
return json({ error: 'Failed to update user' }, { status: 500 });
}
}
async deleteUser(event: RequestEvent) {
try {
const { params } = event;
const storage = this.sdk.storage();
await storage.delete('users', params.id);
return json({ success: true });
} catch (error) {
console.error('Failed to delete user:', error);
return json({ error: 'Failed to delete user' }, { status: 500 });
}
}
private validateUserData(userData: any) {
if (!userData.email || !userData.name) {
throw new Error('Email and name are required');
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(userData.email)) {
throw new Error('Invalid email format');
}
}
}// src/routes/api/users/+server.ts
import { UserController } from '$lib/controllers/UserController';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async (event) => {
const controller = new UserController(event.locals.sdk);
return await controller.getUsers(event);
};
export const POST: RequestHandler = async (event) => {
const controller = new UserController(event.locals.sdk);
return await controller.createUser(event);
};// src/lib/controllers/BaseController.ts
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export abstract class BaseController {
protected sdk: AsterismsBackendSDK;
constructor(sdk: AsterismsBackendSDK) {
this.sdk = sdk;
}
protected async requireAuth(event: RequestEvent) {
if (!event.locals.user) {
return json({ error: 'Unauthorized' }, { status: 401 });
}
return null;
}
protected async requirePermission(permission: string) {
const auth = this.sdk.authorization();
const hasPermission = await auth.hasPermission(permission);
if (!hasPermission) {
return json(
{
error: `Missing permission: ${permission}`
},
{ status: 403 }
);
}
return null;
}
protected handleError(error: any, context: string) {
console.error(`${context}:`, error);
if (error.name === 'ValidationError') {
return json({ error: error.message }, { status: 400 });
}
if (error.name === 'NotFoundError') {
return json({ error: 'Resource not found' }, { status: 404 });
}
return json({ error: 'Internal server error' }, { status: 500 });
}
protected async parseJSON(request: Request) {
try {
return await request.json();
} catch (error) {
throw new Error('Invalid JSON in request body');
}
}
}// src/lib/controllers/ProductController.ts
import { BaseController } from './BaseController';
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
export class ProductController extends BaseController {
async getProducts(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('products.read');
if (permissionCheck) return permissionCheck;
const storage = this.sdk.storage();
const products = await storage.list('products');
return json({ products });
} catch (error) {
return this.handleError(error, 'ProductController.getProducts');
}
}
async createProduct(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('products.create');
if (permissionCheck) return permissionCheck;
const productData = await this.parseJSON(event.request);
// Validate product data
this.validateProductData(productData);
const storage = this.sdk.storage();
const newProduct = await storage.create('products', {
...productData,
createdBy: event.locals.user.id,
createdAt: new Date().toISOString()
});
return json({ product: newProduct }, { status: 201 });
} catch (error) {
return this.handleError(error, 'ProductController.createProduct');
}
}
private validateProductData(productData: any) {
if (!productData.name || !productData.price) {
throw new Error('Name and price are required');
}
if (typeof productData.price !== 'number' || productData.price <= 0) {
throw new Error('Price must be a positive number');
}
}
}// src/lib/services/UserService.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export interface User {
id: string;
email: string;
name: string;
role: 'admin' | 'user' | 'viewer';
createdAt: string;
updatedAt: string;
}
export class UserService {
constructor(private sdk: AsterismsBackendSDK) {}
async findAll(): Promise<User[]> {
const storage = this.sdk.storage();
return await storage.list('users');
}
async findById(id: string): Promise<User | null> {
const storage = this.sdk.storage();
return await storage.get('users', id);
}
async create(userData: Partial<User>): Promise<User> {
const storage = this.sdk.storage();
const user: User = {
id: crypto.randomUUID(),
email: userData.email!,
name: userData.name!,
role: userData.role || 'user',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
return await storage.create('users', user);
}
async update(id: string, userData: Partial<User>): Promise<User> {
const storage = this.sdk.storage();
const updates = {
...userData,
updatedAt: new Date().toISOString()
};
return await storage.update('users', id, updates);
}
async delete(id: string): Promise<void> {
const storage = this.sdk.storage();
await storage.delete('users', id);
}
async findByEmail(email: string): Promise<User | null> {
const storage = this.sdk.storage();
const users = await storage.list('users');
return users.find((user) => user.email === email) || null;
}
}// src/lib/controllers/UserController.ts
import { BaseController } from './BaseController';
import { UserService } from '$lib/services/UserService';
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
export class UserController extends BaseController {
private userService: UserService;
constructor(sdk: AsterismsBackendSDK) {
super(sdk);
this.userService = new UserService(sdk);
}
async getUsers(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('users.read');
if (permissionCheck) return permissionCheck;
const users = await this.userService.findAll();
return json({ users });
} catch (error) {
return this.handleError(error, 'UserController.getUsers');
}
}
async getUser(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const { params } = event;
const user = await this.userService.findById(params.id);
if (!user) {
return json({ error: 'User not found' }, { status: 404 });
}
return json({ user });
} catch (error) {
return this.handleError(error, 'UserController.getUser');
}
}
async createUser(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('users.create');
if (permissionCheck) return permissionCheck;
const userData = await this.parseJSON(event.request);
// Check if user already exists
const existingUser = await this.userService.findByEmail(userData.email);
if (existingUser) {
return json({ error: 'User already exists' }, { status: 409 });
}
const newUser = await this.userService.create(userData);
return json({ user: newUser }, { status: 201 });
} catch (error) {
return this.handleError(error, 'UserController.createUser');
}
}
}// src/lib/dto/UserDto.ts
import { z } from 'zod';
export const CreateUserDto = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['admin', 'user', 'viewer']).default('user')
});
export const UpdateUserDto = z.object({
name: z.string().min(1).max(100).optional(),
role: z.enum(['admin', 'user', 'viewer']).optional()
});
export type CreateUserData = z.infer<typeof CreateUserDto>;
export type UpdateUserData = z.infer<typeof UpdateUserDto>;// src/lib/controllers/UserController.ts
import { CreateUserDto, UpdateUserDto } from '$lib/dto/UserDto';
import { BaseController } from './BaseController';
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
export class UserController extends BaseController {
async createUser(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('users.create');
if (permissionCheck) return permissionCheck;
const requestData = await this.parseJSON(event.request);
// Validate request data
const validationResult = CreateUserDto.safeParse(requestData);
if (!validationResult.success) {
return json(
{
error: 'Validation failed',
details: validationResult.error.errors
},
{ status: 400 }
);
}
const userData = validationResult.data;
const newUser = await this.userService.create(userData);
return json({ user: newUser }, { status: 201 });
} catch (error) {
return this.handleError(error, 'UserController.createUser');
}
}
async updateUser(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('users.update');
if (permissionCheck) return permissionCheck;
const { params } = event;
const requestData = await this.parseJSON(event.request);
// Validate request data
const validationResult = UpdateUserDto.safeParse(requestData);
if (!validationResult.success) {
return json(
{
error: 'Validation failed',
details: validationResult.error.errors
},
{ status: 400 }
);
}
const userData = validationResult.data;
const updatedUser = await this.userService.update(params.id, userData);
return json({ user: updatedUser });
} catch (error) {
return this.handleError(error, 'UserController.updateUser');
}
}
}// src/lib/services/FileUploadService.ts
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export interface UploadResult {
filename: string;
url: string;
size: number;
mimeType: string;
}
export class FileUploadService {
constructor(private sdk: AsterismsBackendSDK) {}
async uploadFile(file: File, path: string): Promise<UploadResult> {
const storage = this.sdk.storage();
// Convert File to ArrayBuffer
const buffer = await file.arrayBuffer();
// Generate unique filename
const filename = `${Date.now()}-${file.name}`;
const fullPath = `${path}/${filename}`;
// Store file
await storage.set(fullPath, buffer);
return {
filename,
url: `/api/files/${encodeURIComponent(fullPath)}`,
size: file.size,
mimeType: file.type
};
}
async getFile(path: string): Promise<ArrayBuffer | null> {
const storage = this.sdk.storage();
return await storage.get(path);
}
async deleteFile(path: string): Promise<void> {
const storage = this.sdk.storage();
await storage.delete(path);
}
}// src/lib/controllers/FileController.ts
import { BaseController } from './BaseController';
import { FileUploadService } from '$lib/services/FileUploadService';
import { json } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
export class FileController extends BaseController {
private fileService: FileUploadService;
constructor(sdk: AsterismsBackendSDK) {
super(sdk);
this.fileService = new FileUploadService(sdk);
}
async uploadFile(event: RequestEvent) {
try {
const authCheck = await this.requireAuth(event);
if (authCheck) return authCheck;
const permissionCheck = await this.requirePermission('files.upload');
if (permissionCheck) return permissionCheck;
const formData = await event.request.formData();
const file = formData.get('file') as File;
if (!file) {
return json({ error: 'No file provided' }, { status: 400 });
}
// Validate file
const validationError = this.validateFile(file);
if (validationError) {
return json({ error: validationError }, { status: 400 });
}
const uploadPath = 'uploads';
const result = await this.fileService.uploadFile(file, uploadPath);
return json({ file: result }, { status: 201 });
} catch (error) {
return this.handleError(error, 'FileController.uploadFile');
}
}
async getFile(event: RequestEvent) {
try {
const { params } = event;
const filePath = decodeURIComponent(params.path);
const fileBuffer = await this.fileService.getFile(filePath);
if (!fileBuffer) {
return json({ error: 'File not found' }, { status: 404 });
}
// Determine content type
const contentType = this.getContentType(filePath);
return new Response(fileBuffer, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=3600'
}
});
} catch (error) {
return this.handleError(error, 'FileController.getFile');
}
}
private validateFile(file: File): string | null {
const maxSize = 10 * 1024 * 1024; // 10MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (file.size > maxSize) {
return 'File too large. Maximum size is 10MB.';
}
if (!allowedTypes.includes(file.type)) {
return 'Invalid file type. Only JPEG, PNG, GIF, and PDF files are allowed.';
}
return null;
}
private getContentType(filePath: string): string {
const extension = filePath.split('.').pop()?.toLowerCase();
switch (extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'pdf':
return 'application/pdf';
default:
return 'application/octet-stream';
}
}
}// src/lib/controllers/WebSocketController.ts
import { BaseController } from './BaseController';
import type { AsterismsBackendSDK } from '@asterisms/sdk-backend';
export class WebSocketController extends BaseController {
private connections = new Map<string, WebSocket>();
constructor(sdk: AsterismsBackendSDK) {
super(sdk);
}
async handleConnection(request: Request, connectionId: string) {
const { socket, response } = Deno.upgradeWebSocket(request);
this.connections.set(connectionId, socket);
socket.onopen = () => {
console.log(`WebSocket connection opened: ${connectionId}`);
this.sendMessage(connectionId, {
type: 'connection',
message: 'Connected successfully'
});
};
socket.onmessage = (event) => {
this.handleMessage(connectionId, JSON.parse(event.data));
};
socket.onclose = () => {
console.log(`WebSocket connection closed: ${connectionId}`);
this.connections.delete(connectionId);
};
socket.onerror = (error) => {
console.error(`WebSocket error for ${connectionId}:`, error);
this.connections.delete(connectionId);
};
return response;
}
private async handleMessage(connectionId: string, message: any) {
try {
switch (message.type) {
case 'subscribe':
await this.handleSubscribe(connectionId, message.channel);
break;
case 'unsubscribe':
await this.handleUnsubscribe(connectionId, message.channel);
break;
case 'broadcast':
await this.handleBroadcast(connectionId, message);
break;
default:
this.sendMessage(connectionId, {
type: 'error',
message: 'Unknown message type'
});
}
} catch (error) {
console.error('WebSocket message handling error:', error);
this.sendMessage(connectionId, {
type: 'error',
message: 'Failed to process message'
});
}
}
private async handleSubscribe(connectionId: string, channel: string) {
// Store subscription in storage
const storage = this.sdk.storage();
const subscriptions = (await storage.get('subscriptions', connectionId)) || [];
if (!subscriptions.includes(channel)) {
subscriptions.push(channel);
await storage.set('subscriptions', connectionId, subscriptions);
}
this.sendMessage(connectionId, {
type: 'subscribed',
channel
});
}
private async handleUnsubscribe(connectionId: string, channel: string) {
const storage = this.sdk.storage();
const subscriptions = (await storage.get('subscriptions', connectionId)) || [];
const updatedSubscriptions = subscriptions.filter((sub) => sub !== channel);
await storage.set('subscriptions', connectionId, updatedSubscriptions);
this.sendMessage(connectionId, {
type: 'unsubscribed',
channel
});
}
private async handleBroadcast(connectionId: string, message: any) {
const { channel, data } = message;
// Get all connections subscribed to this channel
const storage = this.sdk.storage();
const allSubscriptions = await storage.list('subscriptions');
for (const [connId, subscriptions] of allSubscriptions) {
if (subscriptions.includes(channel) && connId !== connectionId) {
this.sendMessage(connId, {
type: 'broadcast',
channel,
data
});
}
}
}
private sendMessage(connectionId: string, message: any) {
const socket = this.connections.get(connectionId);
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
}
}
public broadcastToChannel(channel: string, data: any) {
// This method can be called from other parts of the application
// to broadcast messages to all subscribers of a channel
this.connections.forEach((socket, connectionId) => {
this.sendMessage(connectionId, {
type: 'broadcast',
channel,
data
});
});
}
}// src/lib/controllers/UserController.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { UserController } from './UserController';
import { UserService } from '$lib/services/UserService';
// Mock SDK
const mockSDK = {
storage: () => ({
list: vi.fn(),
get: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn()
}),
authorization: () => ({
hasPermission: vi.fn().mockResolvedValue(true)
})
};
describe('UserController', () => {
let controller: UserController;
let mockEvent: any;
beforeEach(() => {
controller = new UserController(mockSDK as any);
mockEvent = {
locals: {
sdk: mockSDK,
user: { id: 'user123' }
},
request: {
json: vi.fn()
},
params: {}
};
});
it('should get users successfully', async () => {
const mockUsers = [
{ id: '1', email: 'user1@example.com', name: 'User 1' },
{ id: '2', email: 'user2@example.com', name: 'User 2' }
];
mockSDK.storage().list.mockResolvedValue(mockUsers);
const response = await controller.getUsers(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(200);
expect(responseData.users).toEqual(mockUsers);
});
it('should create user successfully', async () => {
const userData = {
email: 'newuser@example.com',
name: 'New User',
role: 'user'
};
const createdUser = { id: '3', ...userData };
mockEvent.request.json.mockResolvedValue(userData);
mockSDK.storage().create.mockResolvedValue(createdUser);
const response = await controller.createUser(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(201);
expect(responseData.user).toEqual(createdUser);
});
it('should handle unauthorized access', async () => {
mockEvent.locals.user = null;
const response = await controller.getUsers(mockEvent);
const responseData = await response.json();
expect(response.status).toBe(401);
expect(responseData.error).toBe('Unauthorized');
});
});// src/routes/api/users/+server.test.ts
import { describe, it, expect } from 'vitest';
import { GET, POST } from './+server';
describe('/api/users', () => {
it('should return users list', async () => {
const mockEvent = {
locals: {
sdk: {
storage: () => ({
list: async () => [{ id: '1', email: 'test@example.com', name: 'Test User' }]
})
},
user: { id: 'user123' }
}
};
const response = await GET(mockEvent as any);
const data = await response.json();
expect(response.status).toBe(200);
expect(data.users).toBeDefined();
expect(data.users).toHaveLength(1);
});
it('should create new user', async () => {
const userData = {
email: 'newuser@example.com',
name: 'New User',
role: 'user'
};
const mockEvent = {
locals: {
sdk: {
storage: () => ({
create: async (collection, data) => ({ id: '2', ...data })
}),
authorization: () => ({
hasPermission: async () => true
})
},
user: { id: 'user123' }
},
request: {
json: async () => userData
}
};
const response = await POST(mockEvent as any);
const data = await response.json();
expect(response.status).toBe(201);
expect(data.user.email).toBe(userData.email);
});
});// ✅ Good: Consistent error handling
export class UserController extends BaseController {
async getUser(event: RequestEvent) {
try {
const user = await this.userService.findById(event.params.id);
if (!user) {
return json({ error: 'User not found' }, { status: 404 });
}
return json({ user });
} catch (error) {
return this.handleError(error, 'UserController.getUser');
}
}
}
// ❌ Bad: Inconsistent error handling
export const GET: RequestHandler = async ({ params }) => {
try {
const user = await getUserById(params.id);
return json({ user });
} catch (error) {
// Different error handling in each route
return json({ message: 'Error occurred' }, { status: 500 });
}
};// ✅ Good: Use schema validation
const validationResult = CreateUserDto.safeParse(requestData);
if (!validationResult.success) {
return json(
{
error: 'Validation failed',
details: validationResult.error.errors
},
{ status: 400 }
);
}
// ❌ Bad: Manual validation
if (!userData.email || !userData.name) {
return json({ error: 'Missing fields' }, { status: 400 });
}// ✅ Good: Proper resource cleanup
export class FileController extends BaseController {
async uploadFile(event: RequestEvent) {
let tempFile: string | null = null;
try {
const formData = await event.request.formData();
const file = formData.get('file') as File;
// Process file...
return json({ success: true });
} catch (error) {
// Clean up temp file if it exists
if (tempFile) {
await fs.unlink(tempFile);
}
return this.handleError(error, 'FileController.uploadFile');
}
}
}