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, themethodargument 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 withcreateMiddleware. Returned middleware context is merged intoctx.context.requireHeaders: Whentrue, requiresheadersto be provided when calling the endpoint as a function. (No effect for HTTP requests.)requireRequest: Whentrue, requires arequestobject 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 requestContent-Typevalues 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 rawRequestobject (when running via HTTP).ctx.headers:Headersinstance for the incoming request.ctx.body: Parsed request body (based onContent-Type).ctx.query: Parsed and validated query string (whenqueryschema 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 bycreateMiddleware.
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.
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:
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.
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.
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.