Skip to content

Repository files navigation

IOServer

npm versionLicense: Apache-2.0CI Build and TestTypeScript

A TypeScript framework for building real-time applications, combining Fastify (HTTP) and Socket.IO (WebSocket) behind a single unified API.

Overview

IOServer structures your application around five component types — Services, Controllers, Managers, Watchers, and Middlewares — each with a well-defined responsibility. Components are registered on the IOServer instance before startup; the framework wires routing, CORS, and Socket.IO transport automatically.

It is designed to be small, explicit, and easily testable:

  • No magic decorators or code generation
  • Route definitions are plain JSON files, kept separate from handler logic
  • Managers are injectable singletons available to every component via AppHandle
  • All base classes expose a minimal surface; you only override what you need

Architecture

graph TB
subgraph "Clients"
HTTP[HTTP Clients]
WS[WebSocket Clients]
end
subgraph "IOServer"
direction TB
Fastify[Fastify HTTP layer]
SocketIO[Socket.IO layer]
subgraph "Components"
MW[Middlewares]
CTL[Controllers]
SVC[Services]
MGR[Managers]
WCH[Watchers]
end
end
HTTP -->|REST requests| Fastify
WS -->|WS upgrade| SocketIO
Fastify --> MW
SocketIO --> MW
MW --> CTL
MW --> SVC
CTL --> MGR
SVC --> MGR
WCH --> MGR
Loading

Key Features

  • Unified HTTP + WebSocket — Fastify v5 and Socket.IO v4 share the same port and TLS configuration
  • Component model — Five explicit roles (Service, Controller, Manager, Watcher, Middleware) keep business logic isolated and testable
  • JSON route files — HTTP routes are declared in .json files; no annotations or meta-programming required
  • Injectable managers — Singleton managers are exposed to all components through a typed AppHandle, avoiding global state
  • TypeScript native — Ships with declaration files; strict mode compatible
  • CORS built-in — Pass a standard Fastify CORS options object; applied to both HTTP and Socket.IO handshake
  • Configurable transports — Choose websocket, polling, or both for Socket.IO
  • SPA fallback — Optional static file serving with single-page application fallback routing

Requirements

  • Node.js 18+
  • TypeScript 5.0+
  • pnpm (recommended) or npm / yarn

Installation

npm install ioserver
# or
pnpm add ioserver

Quick Start

import{IOServer,BaseService,BaseController,BaseManager}from'ioserver';// --- Manager: shared state ---classAppManagerextendsBaseManager{privatecount=0;increment(){this.count++;}getCount(){returnthis.count;}}// --- Service: WebSocket events ---classChatServiceextendsBaseService{asyncsendMessage(socket: any,data: {text: string},callback?: Function){constmgr=this.app.getManager('app')asAppManager;mgr.increment();socket.broadcast.emit('message',{text: data.text,total: mgr.getCount()});if(callback)callback({status: 'ok'});}}// --- Controller: HTTP endpoints ---classStatsControllerextendsBaseController{asyncgetStats(request: any,reply: any){constmgr=this.app.getManager('app')asAppManager;reply.send({messages: mgr.getCount()});}}// --- Bootstrap ---constserver=newIOServer({host: 'localhost',port: 3000});server.addManager({name: 'app',manager: AppManager});server.addService({name: 'chat',service: ChatService});server.addController({name: 'stats',controller: StatsController});awaitserver.start();

Managers must be registered before Services and Controllers so that AppHandle references are already populated at startup.

Components

Services — WebSocket event handlers

A Service groups Socket.IO event handlers. Each public method of the class is automatically bound to the Socket.IO event <method_name>.

import{BaseService}from'ioserver';importtype{Socket}from'socket.io';classRoomServiceextendsBaseService{asyncjoin(socket: Socket,data: {room: string},callback?: Function){socket.join(data.room);socket.to(data.room).emit('user_joined',{id: socket.id});if(callback)callback({joined: data.room});}asyncleave(socket: Socket,data: {room: string}){socket.leave(data.room);}}server.addService({name: 'room',service: RoomService});

Registration options:

OptionTypeDescription
namestringNamespace name (used as Socket.IO namespace /name)
servicetypeof BaseServiceService class (not an instance)
middlewaresBaseMiddleware[]Optional middleware chain for this namespace

Controllers — HTTP route handlers

A Controller groups Fastify route handlers. Routes are mapped through a JSON file located in the routes/ directory (or the path set in options.routes).

import{BaseController}from'ioserver';importtype{FastifyRequest,FastifyReply}from'fastify';classUserControllerextendsBaseController{asyncgetUser(request: FastifyRequest<{Params: {id: string}}>,reply: FastifyReply){reply.send({id: request.params.id});}asynccreateUser(request: FastifyRequest<{Body: {name: string}}>,reply: FastifyReply){reply.code(201).send({id: crypto.randomUUID(),name: request.body.name});}}server.addController({name: 'user',controller: UserController});

Corresponding route file routes/user.json:

[
{ "method": "GET", "url": "/users/:id", "handler": "getUser" },
{ "method": "POST", "url": "/users", "handler": "createUser" }
]

Registration options:

OptionTypeDescription
namestringMust match the JSON route file basename (routes/<name>.json)
controllertypeof BaseControllerController class (not an instance)
middlewaresBaseMiddleware[]Optional middleware chain for all routes of this controller

Managers — Injectable singletons

Managers hold shared state and business logic. They are instantiated once and exposed to every Service, Controller, and Watcher through this.app.

import{BaseManager}from'ioserver';classCacheManagerextendsBaseManager{privatestore=newMap<string,unknown>();set(key: string,value: unknown){this.store.set(key,value);}get(key: string){returnthis.store.get(key);}has(key: string){returnthis.store.has(key);}}server.addManager({name: 'cache',manager: CacheManager});// In any other component:constcache=this.app.getManager('cache')asCacheManager;cache.set('session:42',{userId: 42});

The optional start() method is called automatically by the framework after all components are registered and before the server begins accepting connections.

Watchers — Background tasks

Watchers run independent background loops. Both watch() and stop() must be implemented.

import{BaseWatcher}from'ioserver';classCleanupWatcherextendsBaseWatcher{privatetimer: ReturnType<typeofsetInterval>|null=null;asyncwatch(){this.timer=setInterval(async()=>{constcache=this.app.getManager('cache')asCacheManager;// periodic cleanup logic},60_000);}stop(){if(this.timer){clearInterval(this.timer);this.timer=null;}}}server.addWatcher({name: 'cleanup',watcher: CleanupWatcher});

Middlewares — Request and connection guards

Middlewares intercept HTTP requests (Fastify preHandler) and Socket.IO connections before they reach Controllers or Services.

import{BaseMiddleware}from'ioserver';importtype{FastifyRequest,FastifyReply}from'fastify';importtype{Socket}from'socket.io';classAuthMiddlewareextendsBaseMiddleware{// HTTP guardasynchandle(request: FastifyRequest,reply: FastifyReply,next: Function){consttoken=request.headers.authorization?.split(' ')[1];if(!token||!this.verify(token)){returnreply.code(401).send({error: 'Unauthorized'});}next();}// WebSocket guardasynchandleSocket(socket: Socket,next: Function){consttoken=socket.handshake.auth?.token;if(!token||!this.verify(token)){returnnext(newError('Unauthorized'));}next();}privateverify(token: string){/* JWT verification */returntrue;}}// Apply to a specific controller or serviceserver.addController({name: 'admin',controller: AdminController,middlewares: [AuthMiddleware]});

Configuration

constserver=newIOServer(options);
OptionTypeDefaultDescription
hoststring'localhost'Bind address
portnumber8080Listen port
verbosestring'ERROR'Log level (DEBUG, INFO, WARNING, ERROR)
cookiebooleanfalseEnable Socket.IO cookies
modestring | string[]['websocket','polling']Socket.IO transport(s)
corsobjectundefinedFastify CORS options (applied to HTTP and Socket.IO)
routesstring'./routes'Directory containing JSON route files
rootDirstring'.'Root directory for static file serving
spaFallbackbooleanfalseServe index.html for unmatched routes (SPA mode)

CORS example

constserver=newIOServer({host: '0.0.0.0',port: 8080,cors: {origin: ['https://app.example.com'],methods: ['GET','POST','PUT','DELETE'],credentials: true,},});

Testing

# Run all tests
pnpm test# With coverage report
pnpm run test:coverage
# Isolated suites
pnpm run test:unit
pnpm run test:integration
pnpm run test:e2e
pnpm run test:performance

Coverage targets: 90% statements, 85% branches, 90% functions.

Examples

Simple server

examples/simple.ts — all five component types in a single file, useful as a project template:

pnpm run dev:simple

Chat application

examples/chat-app/ — a complete multi-room chat server with:

  • RoomService — join/leave/message Socket.IO events
  • ChatController — REST endpoints for room history and statistics
  • StatsManager — shared counters accessible from both layers
  • ChatWatcher — periodic inactive-room cleanup
pnpm run dev:chat
# orcd examples/chat-app && ts-node app.ts

Connect at http://localhost:8080.

Project Organization

ioserver/
├── src/
│ ├── IOServer.ts # Main class — startup, registration, routing
│ ├── BaseClasses.ts # BaseService, BaseController, BaseManager,
│ │ # BaseWatcher, BaseMiddleware
│ ├── IOServerError.ts # Error hierarchy
│ └── index.ts # Public exports
├── examples/
│ ├── simple.ts # Minimal example (all component types)
│ └── chat-app/ # Full chat application
├── tests/
│ ├── unit/
│ ├── integration/
│ ├── e2e/
│ └── performance/
├── docs-site/ # Nuxt/Docus documentation site
├── tsconfig.json
└── package.json

Docker Deployment

FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json .
EXPOSE 8080
CMD ["node", "dist/index.js"]
# compose.ymlservices:
app:
build: .ports:
- "8080:8080"environment:
NODE_ENV: productionrestart: unless-stopped

Related Projects

  • uPKI CA Server — Certificate Authority built on this framework pattern
  • uPKI RA Server — Registration Authority built on this framework pattern

Contributing

See CONTRIBUTING.md for the development setup, component rules, test strategy, and commit conventions.

License

Apache-2.0 — see LICENSE.

About

Damn simple Fastify & Socket.io server framework with TypeScript support

Topics

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages