Skip to main content
The API server is the core backend service handling all data operations for SkyTeam ROBLOX, built with Express and TypeScript.

Overview

The API provides authenticated endpoints for airline operations, flight management, and user data access. Package: @skyteam/api
Location: apps/api
Port: 4000 (default)

Technology Stack

  • Runtime: Node.js with TypeScript
  • Framework: Express 4.18
  • Security: Helmet (security headers), CORS
  • Database: Prisma ORM via @skyteam/database
  • Build Tool: tsup for fast TypeScript compilation

Architecture

Server Setup

The main server (src/index.ts:1) configures middleware and routes:
apps/api/src/index.ts
import express from "express";
import helmet from "helmet";
import cors from "cors";

import { airlineAuth } from "./middleware/auth";
import statusRouter from "./routes/status";
import airlineRouter from "./routes/airline";
import flightRouter from "./routes/flight";
import usersRouter from "./routes/users";

const app = express();

// Security & basics
app.use(helmet());
app.use(cors());
app.use(express.json());

// Public health (no auth) to diagnose server without DB
app.get("/health", (_req, res) => {
  res.json({ ok: true });
});

// Auth (all routes require a valid airline token via x-api-key)
app.use(airlineAuth);

// Routes
app.use(statusRouter);
app.use(airlineRouter);
app.use(flightRouter);
app.use(usersRouter);

const PORT = process.env.PORT ? Number(process.env.PORT) : 4000;
app.listen(PORT, () => {
  console.log(`[api] listening on http://localhost:${PORT}`);
});

Middleware Stack

  1. Helmet: Sets security HTTP headers
  2. CORS: Enables cross-origin requests
  3. express.json(): Parses JSON request bodies
  4. airlineAuth: Validates API keys for protected routes

Authentication

Airline Authentication

All routes (except /health) require airline authentication via x-api-key header (src/middleware/auth.ts:9):
apps/api/src/middleware/auth.ts
export async function airlineAuth(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  try {
    const token = req.header("x-api-key");
    if (!token) {
      return res.status(401).json({ error: "Missing x-api-key header" });
    }

    const airline = await fetchAirlineByToken(token);
    if (!airline) {
      return res.status(401).json({ error: "Invalid API key" });
    }

    // Attach to locals for downstream handlers
    res.locals.airline = airline;
    res.locals.safeAirline = safeAirline(airline);

    return next();
  } catch (err) {
    return next(err);
  }
}

Token Safety

The safeAirline helper removes sensitive token data:
apps/api/src/middleware/auth.ts
export function safeAirline<T extends { token?: string }>(airline: T) {
  const { token, ...safe } = airline as any;
  return safe;
}

API Routes

Status Routes

File: src/routes/status.ts
GET /status
Returns API and airline status:
{
  "ok": true,
  "milesAvailable": true,
  "airline": { /* airline data */ }
}

Airline Routes

File: src/routes/airline.ts

Get Airline Data

GET /airline
Returns authenticated airline’s data (excluding token):
apps/api/src/routes/airline.ts
router.get("/airline", async (_req, res, next) => {
  try {
    const safeAirline = res.locals.safeAirline ?? res.locals.airline;
    res.json(safeAirline);
  } catch (err) {
    next(err);
  }
});

Get Products

GET /airline/fetchProductsData
Returns airline’s miles products:
apps/api/src/routes/airline.ts
router.get("/airline/fetchProductsData", async (_req, res, next) => {
  try {
    const airline = res.locals.airline as { airlineId: string };
    const products = await fetchMilesProducts(airline.airlineId);
    res.json(products);
  } catch (err) {
    next(err);
  }
});

Flight Routes

File: src/routes/flight.ts

Get Upcoming Flights

GET /flight/fetchUpcomingFlights
Returns upcoming flights with brand information:
apps/api/src/routes/flight.ts
router.get("/flight/fetchUpcomingFlights", async (_req, res, next) => {
  try {
    const airline = res.locals.airline as { airlineId: string };

    const [flights, brands] = await Promise.all([
      fetchComingFlights(airline.airlineId),
      fetchAirlineBrands(airline.airlineId),
    ]);

    const brandsById = new Map(brands.map((b) => [b.brandId, b]));
    const withBrand = flights.map((f) => ({
      ...f,
      brand: brandsById.get(f.brandId) || null,
    }));

    res.json(withBrand);
  } catch (err) {
    next(err);
  }
});

Start Flight

POST /flight/:id/serverStart
Sets startedAt timestamp for a flight:
apps/api/src/routes/flight.ts
router.post("/flight/:id/serverStart", async (req, res, next) => {
  try {
    const { id } = req.params;
    const updated = await startFlight(id);
    if (!updated) return res.status(404).json({ error: "Flight not found" });
    res.json(updated);
  } catch (err) {
    next(err);
  }
});

Flight Heartbeat

POST /flight/:id/tick
Receives periodic heartbeats from active flights:
apps/api/src/routes/flight.ts
router.post("/flight/:id/tick", async (req, res, next) => {
  try {
    const { id } = req.params;
    // In future: persist lastPingAt and auto-end if stale
    res.json({ ok: true, flightId: id, message: "tick received" });
  } catch (err) {
    next(err);
  }
});

End Flight

POST /flight/:id/serverEnd
Sets endTime timestamp for a flight:
apps/api/src/routes/flight.ts
router.post("/flight/:id/serverEnd", async (req, res, next) => {
  try {
    const { id } = req.params;
    const updated = await endFlight(id);
    if (!updated) return res.status(404).json({ error: "Flight not found" });
    res.json(updated);
  } catch (err) {
    next(err);
  }
});

User Routes

File: src/routes/users.ts

Fetch Multiple Users

POST /users/fetchUsersData
Batch fetches user data and flight history:
// Request body
{
  "userIds": ["123", "456", "789"]
}
// or
["123", "456", "789"]
apps/api/src/routes/users.ts
router.post("/users/fetchUsersData", async (req, res, next) => {
  try {
    let idsInput: unknown = req.body;
    let userIds: string[] = [];

    if (Array.isArray(idsInput)) {
      userIds = idsInput as string[];
    } else if (
      idsInput &&
      typeof idsInput === "object" &&
      Array.isArray((idsInput as any).userIds)
    ) {
      userIds = (idsInput as any).userIds as string[];
    }

    userIds = (userIds || [])
      .map((v) => String(v || "").trim())
      .filter(Boolean);

    if (userIds.length === 0) {
      return res.status(400).json({
        error: "Provide a list of userIds in the request body",
      });
    }

    const uniqueIds = Array.from(new Set(userIds));
    const resultsMap = new Map();

    await Promise.all(
      uniqueIds.map(async (uid) => {
        const [user, flights] = await Promise.all([
          fetchUser(uid),
          fetchUserFlights(uid),
        ]);
        resultsMap.set(uid, { user, flights });
      }),
    );

    // Build output aligned with requested order
    const data = userIds.map((uid) => {
      const entry = resultsMap.get(uid);
      if (!entry || !entry.user) {
        return { userId: uid, user: null, flights: [] };
      }
      return { userId: uid, user: entry.user, flights: entry.flights };
    });

    res.json({ data });
  } catch (err) {
    next(err);
  }
});

Buy Product

POST /user/:id/buyProduct
Purchases a miles product for a user:
{
  "productId": "prod_123"
}
apps/api/src/routes/users.ts
router.post("/user/:id/buyProduct", async (req, res, next) => {
  try {
    const { id: userId } = req.params;
    const { productId } = req.body as { productId?: string };
    if (!productId)
      return res.status(400).json({ error: "Missing productId in body" });

    const airline = res.locals.airline as { airlineId: string };
    const [user, products] = await Promise.all([
      fetchUser(userId),
      fetchMilesProducts(airline.airlineId),
    ]);
    if (!user) return res.status(404).json({ error: "User not found" });

    const product = products.find(
      (p) => p.productId === productId && p.active !== false,
    );
    if (!product) return res.status(404).json({ error: "Product not found" });

    const updated = await spendMiles(
      userId,
      product.priceMiles,
      `Purchase: ${product.name}`,
    );

    res.json({ ok: true, user: updated, product });
  } catch (err) {
    if (err instanceof Error && err.message === "Insufficient miles") {
      return res.status(400).json({ error: "Insufficient miles" });
    }
    next(err);
  }
});

Error Handling

Centralized error handler catches all unhandled errors:
apps/api/src/index.ts
app.use(
  (err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
    console.error("Unhandled error:", err);
    res.status(500).json({ error: "Internal Server Error" });
  },
);

Development

Scripts

# Development with hot reload
pnpm dev

# Production build
pnpm build

# Start production server
pnpm start

# Lint code
pnpm lint

Dependencies

apps/api/package.json
{
  "dependencies": {
    "@skyteam/database": "workspace:*",
    "cors": "^2.8.5",
    "express": "^4.18.3",
    "helmet": "^7.1.0"
  }
}

Database Integration

The API uses @skyteam/database package for all data operations:
import {
  fetchAirlineByToken,
  fetchComingFlights,
  fetchUser,
  fetchUserFlights,
  fetchMilesProducts,
  spendMiles,
  startFlight,
  endFlight,
} from "@skyteam/database";
See Database Schema for schema and query details.

Security Features

  • Helmet: Sets security headers (XSS protection, CSP, etc.)
  • CORS: Configurable cross-origin resource sharing
  • Token Authentication: All routes protected by API key validation
  • Input Validation: Request body validation before processing
  • Error Sanitization: Generic error messages to prevent information leakage

Performance Optimizations

  • Parallel Queries: Uses Promise.all() for concurrent database operations
  • Connection Pooling: Database connection pooling via Prisma
  • Deduplication: User fetch requests deduplicated to reduce queries
  • Efficient Mapping: Uses Map for O(1) lookups in data transformations

Deployment

Recommended deployment setup:
  • Hosting: Any Node.js platform (Railway, Render, Fly.io)
  • Environment: Production environment variables
  • Monitoring: Error tracking and performance monitoring
  • Scaling: Horizontal scaling with load balancer

Build docs developers (and LLMs) love