JSPM

  • Created
  • Published
  • Downloads 1769
  • Score
    100M100P100Q105802F
  • License MIT

Core HTTP server for creating REST APIs.

Package Exports

  • @alepha/server

Readme

Alepha Server

Core HTTP server for creating REST APIs.

Installation

This package is part of the Alepha framework and can be installed via the all-in-one package:

npm install alepha

Module

Provides high-performance HTTP server capabilities with declarative routing and action descriptors.

The server module enables building REST APIs and web applications using $route and $action descriptors on class properties. It provides automatic request/response handling, schema validation, middleware support, and seamless integration with other Alepha modules for a complete backend solution.

This module can be imported and used as follows:

import { Alepha, run } from "alepha";
import { AlephaServer } from "alepha/server";

const alepha = Alepha.create()
    .with(AlephaServer);

run(alepha);

API Reference

Descriptors

Descriptors are functions that define and configure various aspects of your application. They follow the convention of starting with $ and return configured descriptor instances.

For more details, see the Descriptors documentation.

$action()

Creates a server action descriptor for defining type-safe HTTP endpoints.

Server actions are the core building blocks for REST APIs in the Alepha framework. They provide a declarative way to define HTTP endpoints with full TypeScript type safety, automatic schema validation, and integrated security features. Actions automatically handle routing, request parsing, response serialization, and OpenAPI documentation generation.

Key Features

  • Type Safety: Full TypeScript inference for request/response types
  • Schema Validation: Automatic validation using TypeBox schemas
  • Auto-routing: Convention-based URL generation with customizable paths
  • Multiple Invocation: Call directly (run()) or via HTTP (fetch())
  • OpenAPI Integration: Automatic documentation generation
  • Security Integration: Built-in authentication and authorization support
  • Content Type Detection: Automatic handling of JSON, form-data, and plain text

URL Generation

By default, actions are prefixed with /api (configurable via SERVER_API_PREFIX):

  • Property name becomes the endpoint path
  • Path parameters are automatically detected from schema
  • HTTP method defaults to GET, or POST if body schema is provided

Use Cases

Perfect for building robust REST APIs:

  • CRUD operations with full type safety
  • File upload and download endpoints
  • Real-time data processing APIs
  • Integration with external services
  • Microservice communication
  • Admin and management interfaces

Basic CRUD operations:

import { $action } from "alepha/server";
import { t } from "alepha";

class UserController {
  // GET /api/users
  getUsers = $action({
    description: "Retrieve all users with pagination",
    schema: {
      query: t.object({
        page: t.optional(t.number({ default: 1 })),
        limit: t.optional(t.number({ default: 10, maximum: 100 })),
        search: t.optional(t.string())
      }),
      response: t.object({
        users: t.array(t.object({
          id: t.string(),
          name: t.string(),
          email: t.string(),
          createdAt: t.datetime()
        })),
        total: t.number(),
        hasMore: t.boolean()
      })
    },
    handler: async ({ query }) => {
      const { page, limit, search } = query;
      const users = await this.userService.findUsers({ page, limit, search });

      return {
        users: users.items,
        total: users.total,
        hasMore: (page * limit) < users.total
      };
    }
  });

  // POST /api/users
  createUser = $action({
    description: "Create a new user account",
    schema: {
      body: t.object({
        name: t.string({ minLength: 2, maxLength: 100 }),
        email: t.string({ format: "email" }),
        password: t.string({ minLength: 8 }),
        role: t.optional(t.enum(["user", "admin"]))
      }),
      response: t.object({
        id: t.string(),
        name: t.string(),
        email: t.string(),
        role: t.string(),
        createdAt: t.datetime()
      })
    },
    handler: async ({ body }) => {
      // Password validation and hashing
      await this.authService.validatePassword(body.password);
      const hashedPassword = await this.authService.hashPassword(body.password);

      // Create user with default role
      const user = await this.userService.create({
        ...body,
        password: hashedPassword,
        role: body.role || "user"
      });

      // Return user without password
      const { password, ...publicUser } = user;
      return publicUser;
    }
  });

  // GET /api/users/:id
  getUser = $action({
    description: "Retrieve user by ID",
    schema: {
      params: t.object({
        id: t.string()
      }),
      response: t.object({
        id: t.string(),
        name: t.string(),
        email: t.string(),
        role: t.string(),
        profile: t.optional(t.object({
          bio: t.string(),
          avatar: t.string({ format: "uri" }),
          location: t.string()
        }))
      })
    },
    handler: async ({ params }) => {
      const user = await this.userService.findById(params.id);
      if (!user) {
        throw new Error(`User not found: ${params.id}`);
      }
      return user;
    }
  });

  // PUT /api/users/:id
  updateUser = $action({
    method: "PUT",
    description: "Update user information",
    schema: {
      params: t.object({ id: t.string() }),
      body: t.object({
        name: t.optional(t.string({ minLength: 2 })),
        email: t.optional(t.string({ format: "email" })),
        profile: t.optional(t.object({
          bio: t.optional(t.string()),
          avatar: t.optional(t.string({ format: "uri" })),
          location: t.optional(t.string())
        }))
      }),
      response: t.object({
        id: t.string(),
        name: t.string(),
        email: t.string(),
        updatedAt: t.datetime()
      })
    },
    handler: async ({ params, body }) => {
      const updatedUser = await this.userService.update(params.id, body);
      return updatedUser;
    }
  });
}

File upload with multipart form data:

class FileController {
  uploadAvatar = $action({
    method: "POST",
    description: "Upload user avatar image",
    schema: {
      body: t.object({
        file: t.file({
          maxSize: 5 * 1024 * 1024, // 5MB
          allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
        }),
        userId: t.string()
      }),
      response: t.object({
        url: t.string({ format: "uri" }),
        size: t.number(),
        mimeType: t.string(),
        uploadedAt: t.datetime()
      })
    },
    handler: async ({ body }) => {
      const { file, userId } = body;

      // Validate file
      await this.fileService.validateImage(file);

      // Generate unique filename
      const filename = `avatars/${userId}/${Date.now()}-${file.name}`;

      // Upload to storage
      const uploadResult = await this.storageService.upload(filename, file);

      // Update user profile
      await this.userService.updateAvatar(userId, uploadResult.url);

      return {
        url: uploadResult.url,
        size: file.size,
        mimeType: file.type,
        uploadedAt: new Date().toISOString()
      };
    }
  });

  downloadFile = $action({
    method: "GET",
    description: "Download file by ID",
    schema: {
      params: t.object({ id: t.string() }),
      query: t.object({
        download: t.optional(t.boolean()),
        thumbnail: t.optional(t.boolean())
      }),
      response: t.file()
    },
    handler: async ({ params, query, reply, user }) => {
      const file = await this.fileService.findById(params.id);
      if (!file) {
        throw new Error("File not found");
      }

      // Check permissions
      await this.fileService.checkAccess(params.id, user.id);

      const fileBuffer = query.thumbnail
        ? await this.fileService.getThumbnail(file.id)
        : await this.fileService.getBuffer(file.path);

      // Set appropriate headers
      reply.header("Content-Type", file.mimeType);
      reply.header("Content-Length", fileBuffer.length);

      if (query.download) {
        reply.header("Content-Disposition", `attachment; filename="${file.name}"`);
      }

      return fileBuffer;
    }
  });
}

Advanced API with custom paths and grouped operations:

class OrderController {
  group = "orders"; // Groups all actions under "orders" tag

  // GET /api/orders/search
  searchOrders = $action({
    name: "search",
    path: "/orders/search", // Custom path
    description: "Advanced order search with filtering",
    schema: {
      query: t.object({
        status: t.optional(t.union([
          t.literal("pending"),
          t.literal("processing"),
          t.literal("shipped"),
          t.literal("delivered"),
          t.literal("cancelled")
        ])),
        customerId: t.optional(t.string()),
        dateFrom: t.optional(t.date()),
        dateTo: t.optional(t.date()),
        minAmount: t.optional(t.number({ minimum: 0 })),
        maxAmount: t.optional(t.number({ minimum: 0 })),
        sortBy: t.optional(t.union([
          t.literal("createdAt"),
          t.literal("amount"),
          t.literal("status")
        ])),
        sortOrder: t.optional(t.enum(["asc", "desc"]))
      }),
      response: t.object({
        orders: t.array(t.object({
          id: t.string(),
          orderNumber: t.string(),
          customerId: t.string(),
          customerName: t.string(),
          status: t.string(),
          totalAmount: t.number(),
          createdAt: t.datetime(),
          itemCount: t.number()
        })),
        pagination: t.object({
          page: t.number(),
          limit: t.number(),
          total: t.number(),
          hasMore: t.boolean()
        }),
        filters: t.object({
          appliedFilters: t.array(t.string()),
          availableStatuses: t.array(t.string())
        })
      })
    },
    handler: async ({ query }) => {
      // Build dynamic query based on filters
      const searchCriteria = this.orderService.buildSearchCriteria(query);
      const results = await this.orderService.searchOrders(searchCriteria);

      return {
        orders: results.orders,
        pagination: results.pagination,
        filters: {
          appliedFilters: Object.keys(query).filter(key => query[key] !== undefined),
          availableStatuses: await this.orderService.getAvailableStatuses()
        }
      };
    }
  });

  // POST /api/orders/:id/process
  processOrder = $action({
    method: "POST",
    path: "/orders/:id/process",
    description: "Process an order through the fulfillment workflow",
    schema: {
      params: t.object({ id: t.string() }),
      body: t.object({
        notes: t.optional(t.string()),
        priority: t.optional(t.union([
          t.literal("low"),
          t.literal("normal"),
          t.literal("high"),
          t.literal("urgent")
        ])),
        assignToWarehouse: t.optional(t.string())
      }),
      response: t.object({
        orderId: t.string(),
        status: t.string(),
        processedAt: t.datetime(),
        estimatedFulfillment: t.datetime(),
        trackingInfo: t.optional(t.object({
          trackingNumber: t.string(),
          carrier: t.string(),
          estimatedDelivery: t.date()
        }))
      })
    },
    handler: async ({ params, body, user }) => {
      // Validate order can be processed
      const order = await this.orderService.findById(params.id);
      if (!order || order.status !== "pending") {
        throw new Error("Order cannot be processed in current status");
      }

      // Check inventory availability
      const inventoryCheck = await this.inventoryService.checkAvailability(order.items);
      if (!inventoryCheck.available) {
        throw new Error(`Insufficient inventory: ${inventoryCheck.missingItems.join(", ")}`);
      }

      // Process the order
      const processResult = await this.fulfillmentService.processOrder({
        orderId: params.id,
        options: {
          notes: body.notes,
          priority: body.priority || "normal",
          warehouse: body.assignToWarehouse
        }
      });

      // Update order status
      await this.orderService.updateStatus(params.id, "processing", {
        processedBy: user.id,
        processedAt: new Date(),
        notes: body.notes
      });

      // Send notification
      await this.notificationService.sendOrderUpdate(order.customerId, {
        orderId: params.id,
        status: "processing",
        message: "Your order is now being processed"
      });

      return {
        orderId: params.id,
        status: "processing",
        processedAt: new Date().toISOString(),
        estimatedFulfillment: processResult.estimatedCompletion,
        trackingInfo: processResult.trackingInfo
      };
    }
  });
}

Actions with security integration and role-based access:

class AdminController {
  group = "admin";

  // Only accessible to users with "admin:users:read" permission
  getUserStats = $action({
    description: "Get comprehensive user statistics",
    security: { permissions: ["admin:users:read"] },
    schema: {
      query: t.object({
        includeInactive: t.optional(t.boolean())
      }),
      response: t.object({
        totalUsers: t.number(),
        activeUsers: t.number(),
        newUsers: t.number(),
        userGrowth: t.number(),
        breakdown: t.object({
          byRole: t.record(t.string(), t.number()),
          byStatus: t.record(t.string(), t.number()),
          byRegistrationSource: t.record(t.string(), t.number())
        }),
        trends: t.array(t.object({
          date: t.date(),
          registrations: t.number(),
          activations: t.number()
        }))
      })
    },
    handler: async ({ query, user }) => {
      // user is available through security integration
      this.auditLogger.log({
        action: "admin.getUserStats",
        userId: user.id,
        userRole: user.role,
        timestamp: new Date()
      });

      const period = query.period || "month";
      const stats = await this.analyticsService.getUserStatistics({
        period,
        includeInactive: query.includeInactive || false
      });

      return stats;
    }
  });

  // Bulk operations with transaction support
  bulkUpdateUsers = $action({
    method: "POST",
    path: "/admin/users/bulk-update",
    description: "Bulk update user properties",
    security: { permissions: ["admin:users:write"] },
    schema: {
      body: t.object({
        userIds: t.array(t.string(), { minItems: 1, maxItems: 1000 }),
        updates: t.object({
          status: t.optional(t.union([t.literal("active"), t.literal("inactive")])),
          role: t.optional(t.string()),
          tags: t.optional(t.array(t.string())),
          customFields: t.optional(t.record(t.string(), t.any()))
        }),
        reason: t.string({ minLength: 10, maxLength: 500 })
      }),
      response: t.object({
        updated: t.number(),
        failed: t.number(),
        errors: t.array(t.object({
          userId: t.string(),
          error: t.string()
        })),
        auditLogId: t.string()
      })
    },
    handler: async ({ body, user }) => {
      const results = { updated: 0, failed: 0, errors: [] };

      // Create audit log entry
      const auditLogId = await this.auditService.logBulkOperation({
        operation: "bulk_user_update",
        initiatedBy: user.id,
        targetCount: body.userIds.length,
        reason: body.reason,
        changes: body.updates
      });

      // Process in batches to avoid overwhelming the database
      const batchSize = 50;
      for (let i = 0; i < body.userIds.length; i += batchSize) {
        const batch = body.userIds.slice(i, i + batchSize);

        try {
          const updateResult = await this.userService.bulkUpdate(batch, body.updates);
          results.updated += updateResult.success;
          results.failed += updateResult.failed;
          results.errors.push(...updateResult.errors);
        } catch (error) {
          // Log batch failure but continue processing
          this.logger.error(`Bulk update batch failed`, {
            batch: i / batchSize + 1,
            userIds: batch,
            error: error.message
          });

          results.failed += batch.length;
          results.errors.push(...batch.map(userId => ({
            userId,
            error: error.message
          })));
        }
      }

      // Update audit log with results
      await this.auditService.updateBulkOperationResults(auditLogId, results);

      return { ...results, auditLogId };
    }
  });
}

Important Notes:

  • Actions are automatically registered with the HTTP server when the service is initialized
  • Use run() for direct invocation (testing, internal calls, or remote services)
  • Use fetch() for explicit HTTP requests (client-side, external services)
  • Schema validation occurs automatically for all requests and responses
  • Path parameters are automatically extracted from schema definitions
  • Content-Type headers are automatically set based on schema types
  • Actions can be disabled via the disabled option for maintenance or feature flags

$route()

Create a basic endpoint.

It's a low level descriptor. You probably want to use $action instead.

Providers

Providers are classes that encapsulate specific functionality and can be injected into your application. They handle initialization, configuration, and lifecycle management.

For more details, see the Providers documentation.

ServerNotReadyProvider

On every request, this provider checks if the server is ready.

If the server is not ready, it responds with a 503 status code and a message indicating that the server is not ready yet.

The response also includes a Retry-After header indicating that the client should retry after 5 seconds.

ServerRouterProvider

Main router for all routes on the server side.

  • $route => generic route
  • $action => action route (for API calls)
  • $page => React route (for SSR)