ServerHTTP Routes

HTTP and API Routes

Since Colyseus 0.17, we introduced another routing stack (a fork of better-call) for client-facing API routes, which enables type inference of HTTP calls to the Client SDK, and visual integration with the Playground. You can use both Express or our own routing stack as you prefer.

Colyseus uses Express and a fork of better-call as its HTTP routing libraries. You can use it to add your custom routes to your server.

API Routes

The built-in router is recommended for new API routes. It supports full type safety both in your backend and your frontend.

import { defineServer, createEndpoint, createRouter } from "colyseus";
import { z } from "zod";
 
const listThings = createEndpoint("/things", { method: "GET" }, /* ... */);
const getThing = createEndpoint("/things/:id", { method: "GET" }, /* ... */);
const createThing = createEndpoint("/things", { method: "POST" }, /* ... */);
const updateThing = createEndpoint("/things/:id", { method: "PUT" }, /* ... */);
const deleteThing = createEndpoint("/things/:id", { method: "DELETE" }, /* ... */);
 
const server = defineServer({
  routes: createRouter({
    listThings,
    getThing,
    createThing,
    updateThing,
    patchThing,
    deleteThing,
  })
})

createEndpoint options

createEndpoint(path, options, handler) accepts the following options. These are the same options supported by the underlying router used in Colyseus.

  • method: HTTP method string or an array of methods (e.g. "GET" or ["GET", "POST"]). When invoking the endpoint directly as a function, the method argument is optional and defaults to the first method in the array.
  • body: Standard schema for validating the request body (e.g. Zod). Invalid bodies result in a 400 response when mounted to the router.
  • query: Standard schema for validating the request query string. Invalid queries result in a 400 response when mounted to the router.
  • use: Array of middlewares created with createMiddleware. Returned middleware context is merged into ctx.context.
  • requireHeaders: When true, requires headers to be provided when calling the endpoint as a function. (No effect for HTTP requests.)
  • requireRequest: When true, requires a request object when calling the endpoint as a function. (No effect for HTTP requests.)
  • metadata: Extra endpoint metadata.
    • scope: Controls visibility for RPC/HTTP routing.
      • "rpc" (default): routed and available to the RPC client.
      • "server": routed and callable directly, but not exposed to the RPC client.
      • "http": routed only (not callable directly or via RPC).
    • allowedMediaTypes: Restricts accepted request Content-Type values for this endpoint (overrides router defaults).

Handler context (ctx)

The handler signature is async (ctx) => response. The context includes request data and helper methods.

Properties

  • ctx.request: The raw Request object (when running via HTTP).
  • ctx.headers: Headers instance for the incoming request.
  • ctx.body: Parsed request body (based on Content-Type).
  • ctx.query: Parsed and validated query string (when query schema is provided).
  • ctx.params: Route parameters and wildcards (e.g. :id, **:name).
  • ctx.method: HTTP method (useful for multi-method endpoints).
  • ctx.context: Merged middleware context returned by createMiddleware.

Methods

  • ctx.setStatus(status): Override the default success status code.
  • ctx.error(codeOrStatus, data?, headers?): Throw an API error with a named code or numeric status. You can include custom response headers.
  • ctx.redirect(url): Throw a redirect response.
  • ctx.json(data): Return a JSON response payload.
  • ctx.setHeader(name, value): Set a response header.
  • ctx.setCookie(name, value, options?): Set a response cookie.
  • ctx.getCookie(name): Read a request cookie.
  • ctx.setSignedCookie(name, value, options?): Set a signed cookie.
  • ctx.getSignedCookie(name): Read a signed cookie.

Examples

1) Body/query validation, params, and multi-method routing

import { createEndpoint } from "colyseus";
import { z } from "zod";
 
export const itemEndpoint = createEndpoint("/item/:id", {
    method: ["GET", "POST"],
    query: z.object({ include: z.string().optional() }),
    body: z.object({ name: z.string() }).optional(),
}, async (ctx) => {
    if (ctx.method === "POST") {
        ctx.setStatus(201);
        return { id: ctx.params.id, name: ctx.body?.name };
    }
 
    return {
        id: ctx.params.id,
        include: ctx.query.include,
    };
});

2) Middleware context, headers, cookies, and custom errors

import { createEndpoint, createMiddleware } from "colyseus";
 
const auth = createMiddleware(async (ctx) => {
    const token = ctx.headers.get("authorization");
    if (!token) throw ctx.error("UNAUTHORIZED", { message: "Missing token" });
    return { userId: "user-123" };
});
 
export const secureEndpoint = createEndpoint("/secure", {
    method: "GET",
    use: [auth],
}, async (ctx) => {
    ctx.setHeader("X-User", ctx.context.userId);
    ctx.setCookie("session", "abc", { httpOnly: true });
    return { ok: true, userId: ctx.context.userId };
});

3) Media types, request/headers requirements, and response helpers

import { createEndpoint } from "colyseus";
import { z } from "zod";
 
export const upload = createEndpoint("/upload", {
    method: "POST",
    body: z.object({ id: z.string() }).optional(),
    requireHeaders: true,
    requireRequest: true,
    metadata: {
        allowedMediaTypes: ["multipart/form-data", "application/octet-stream"],
    },
}, async (ctx) => {
    // ctx.request and ctx.headers are guaranteed here
    if (!ctx.body) throw ctx.error(400, { message: "Missing body" });
    return ctx.json({ ok: true });
});

Express

Express is recommended if you rely on existing software built on top of Express.

app.config.ts
import { defineServer } from "colyseus";
import express from "express";
 
const server = defineServer({
    // ...
    express: (app) => {
        //
        // Include express middlewares (e.g. JSON body parser)
        //
        app.use(express.json({ limit: "100kb" }));
 
        //
        // Define your custom routes here
        //
        app.get("/hello_world", (req, res) => {
            res.json({ hello: "world" });
        });
 
    },
    // ...
});

For more information about Express, check out the Express guides.

Parsing JSON bodies

Here’s an example of how to handle JSON POST requests with both backend parsing and frontend usage:

app.config.ts
import { defineServer } from "colyseus";
import express from "express";
 
const server = defineServer({
    // ...
    express: (app) => {
        // Parse incoming JSON bodies
        app.use(express.json({ limit: "100kb" }));
 
        // Example: User profile update endpoint
        app.post("/api/user/profile", (req, res) => {
            const { name, email, preferences } = req.body;
 
            // Validate required fields
            if (!name || !email) {
                return res.status(400).json({
                    error: "Name and email are required"
                });
            }
 
            // Process the data (e.g., save to database)
            console.log("Updating user profile:", { name, email, preferences });
 
            // Return success response
            res.json({
                success: true,
                message: "Profile updated successfully",
                data: { name, email, preferences }
            });
        });
    },
    // ...
});

CORS (Cross-Origin Resource Sharing)

CORS is enabled by default.

What is CORS? - Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources. CORS also relies on a mechanism by which browsers make a “preflight” request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.

See MDN documentation on CORS for more information ↗

You may customize the default CORS headers if you need to:

import { matchMaker } from "colyseus";
 
matchMaker.controller.DEFAULT_CORS_HEADERS = {
    'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization',
    'Access-Control-Allow-Methods': 'GET,HEAD,PUT,PATCH,POST,DELETE',
    'Access-Control-Allow-Credentials': 'true',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Max-Age': '2592000',
    // ...
}

You may also return custom CORS headers based on request headers sent by the client:

import { matchMaker } from "colyseus";
 
matchMaker.controller.getCorsHeaders = function(requestHeaders) {
    // check for 'requestHeaders' and return custom key-value headers here.
    return {};
}

Frontend usage

Use the client.http.* methods to perform HTTP requests to your server.

See Client SDK → HTTP Requests for more details.

client.ts
import { Client } from "@colyseus/sdk";
 
const client = new Client("http://localhost:2567");
 
// Update user profile
async function updateProfile(name: string, email: string, preferences: any) {
    try {
        const response = await client.http.post("/api/user/profile", {
            body: { name, email, preferences }
        });
 
        console.log("Profile updated:", response);
        return response;
    } catch (error) {
        console.error("Failed to update profile:", error);
        throw error;
    }
}
 
// Submit game score
async function submitScore(playerId: string, score: number, level: number) {
    try {
        const response = await client.http.post("/api/game/score", {
            body: { playerId, score, level, timestamp: Date.now() }
        });
 
        console.log("Score submitted:", response);
        return response;
    } catch (error) {
        console.error("Failed to submit score:", error);
        throw error;
    }
}
 
// Usage examples
updateProfile("John Doe", "john@example.com", { theme: "dark" });
submitScore("player123", 1500, 5);

The client.http.post() method automatically handles JSON serialization and sets the appropriate Content-Type header. The backend express.json() middleware will parse the incoming JSON body into req.body.

Last updated on