@sjoblomtobias/relay 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,21 +1,20 @@
1
- MIT License
1
+ Pirate License
2
2
 
3
- Copyright (c) 2025 Tobias Sjöblom
3
+ Copyright (c) 2026 Tobias Sjöblom
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ Avast! Permission be hereby granted, free o’ charge, to any scallywag obtainin’ a copy
6
+ o’ this software and its associated treasure maps (the “Software”), to plunder and deal
7
+ with it without restriction, includin’ without limit the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and sell
9
+ copies o’ the Software, and to allow other swashbucklers to do the same,
10
+ subject to the followin’ code of the sea:
11
11
 
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be hoisted on all
13
+ copies or substantial portions o’ the Software.
14
14
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15
+ THE SOFTWARE BE PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDIN’ BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE, OR NONINFRINGEMENT. IN NO EVENT SHALL THE
18
18
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISIN’ FROM,
20
+ OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS THEREIN.
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # relay
2
+
3
+ A lightweight TypeScript framework for building structured Express applications with decorator-based routing and automatic route loading.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sjoblomtobias/relay
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### 1. Create a route
14
+
15
+ Extend the `Route` class and decorate it with `@Public` or `@Private`. Override any of the built-in HTTP method handlers you need.
16
+
17
+ ```ts
18
+ // src/routes/users.ts
19
+ import { Public, Route } from "@sjoblomtobias/relay";
20
+ import type { Request, Response } from "express";
21
+
22
+ @Public("/users")
23
+ export default class UsersRoute extends Route {
24
+ protected get(req: Request, res: Response): void {
25
+ res.json([{ id: 1, name: "Alice" }]);
26
+ }
27
+
28
+ protected getById(req: Request, res: Response): void {
29
+ res.json({ id: req.params.id });
30
+ }
31
+
32
+ protected post(req: Request, res: Response): void {
33
+ res.status(201).json(req.body);
34
+ }
35
+
36
+ protected patch(req: Request, res: Response): void {
37
+ res.json({ id: req.params.id, ...req.body });
38
+ }
39
+
40
+ protected delete(req: Request, res: Response): void {
41
+ res.status(204).send();
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### 2. Decorators
47
+
48
+ | Decorator | Description |
49
+ |-----------|-------------|
50
+ | `@Public(path)` | Registers the route at `path` without authentication |
51
+ | `@Private(path)` | Registers the route at `path`, applies `authMiddleware` if provided |
52
+ | `@Location(path, isPrivate?)` | Base decorator used by `@Public` and `@Private` |
53
+
54
+ ### 3. Start the server
55
+
56
+ ```ts
57
+ // src/index.ts
58
+ import { Server } from "@sjoblomtobias/relay";
59
+
60
+ const server = new Server(3000, "routes");
61
+
62
+ await server.start(
63
+ async () => {
64
+ // Runs before the server binds — load routes, connect to DB, etc.
65
+ await server.loadRoutes(import.meta.dirname);
66
+ },
67
+ () => {
68
+ console.log("Ready!");
69
+ }
70
+ );
71
+ ```
72
+
73
+ ### 4. Authentication middleware
74
+
75
+ Pass an Express `RequestHandler` as the third argument to `Server`. It will be applied to all routes decorated with `@Private`.
76
+
77
+ ```ts
78
+ import { Server } from "@sjoblomtobias/relay";
79
+
80
+ const authMiddleware = (req, res, next) => {
81
+ if (!req.headers.authorization) return res.status(401).send("Unauthorized");
82
+ next();
83
+ };
84
+
85
+ const server = new Server(3000, "routes", authMiddleware);
86
+ ```
87
+
88
+ You can also set or update it later:
89
+
90
+ ```ts
91
+ server.setAuthMiddleware(authMiddleware);
92
+ ```
93
+
94
+ ## API
95
+
96
+ ### `Server`
97
+
98
+ | Method | Description |
99
+ |--------|-------------|
100
+ | `new Server(port, routesDir, authMiddleware?)` | Creates a new server instance |
101
+ | `loadRoutes(baseDir?)` | Dynamically imports all route files from `baseDir/routesDir`. Defaults `baseDir` to `process.cwd()` |
102
+ | `start(before?, after?)` | Starts the server. Awaits optional `before` callback, binds the port, then awaits optional `after` callback |
103
+ | `use(...handlers)` | Adds Express middleware |
104
+ | `getApp()` | Returns the underlying Express app |
105
+ | `setAuthMiddleware(middleware)` | Sets or replaces the authentication middleware |
106
+
107
+ ### `Route`
108
+
109
+ Abstract base class. Override any of the following optional methods:
110
+
111
+ | Method | HTTP method | Path |
112
+ |--------|-------------|------|
113
+ | `get` | GET | `/location` |
114
+ | `getById` | GET | `/location/:id` |
115
+ | `post` | POST | `/location` |
116
+ | `patch` | PATCH | `/location/:id` |
117
+ | `delete` | DELETE | `/location/:id` |
118
+
119
+ Override `getEndpoints()` to fully customize the endpoint configuration.
120
+
121
+ ## License
122
+
123
+ Pirate
package/dist/index.d.ts CHANGED
@@ -1,7 +1,2 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Entry point for the application.
5
- */
6
1
  export * from "./route.js";
7
2
  export * from "./server.js";
package/dist/index.js CHANGED
@@ -1,7 +1,2 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Entry point for the application.
5
- */
6
1
  export * from "./route.js";
7
2
  export * from "./server.js";
package/dist/route.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Base class for defining routes in an Express application.
5
- */
6
1
  import { Router } from "express";
7
2
  import type { Request, Response, NextFunction } from "express";
8
3
  /**
package/dist/route.js CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Base class for defining routes in an Express application.
5
- */
6
1
  import { Router } from "express";
7
2
  /**
8
3
  * Decorator to set the base path and authentication requirement for a route class.
package/dist/server.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Class representing an Express server that dynamically loads routes from a specified directory.
5
- */
6
1
  import express from "express";
7
2
  import type { RequestHandler } from "express";
8
3
  export declare class Server {
@@ -19,9 +14,10 @@ export declare class Server {
19
14
  constructor(port: number, routesDirectory: string, authMiddleware?: RequestHandler);
20
15
  /**
21
16
  * Loads all routes from the specified directory.
17
+ * @param baseDir The base directory to resolve the routes directory from. Defaults to process.cwd().
22
18
  * @returns Promise that resolves when all routes are loaded.
23
19
  */
24
- loadRoutes(): Promise<void>;
20
+ loadRoutes(baseDir?: string): Promise<void>;
25
21
  /**
26
22
  * Adds middleware functions to the Express application.
27
23
  * @param handlers - Array of Express request handlers (middleware functions).
@@ -34,7 +30,9 @@ export declare class Server {
34
30
  getApp(): express.Express;
35
31
  /**
36
32
  * Starts the Express server.
33
+ * @param before Optional callback invoked before the server starts.
34
+ * @param after Optional callback invoked after the server has started.
37
35
  */
38
- start(): void;
36
+ start(before?: () => void | Promise<void>, after?: () => void | Promise<void>): Promise<void>;
39
37
  setAuthMiddleware(middleware: RequestHandler): void;
40
38
  }
package/dist/server.js CHANGED
@@ -1,8 +1,12 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Class representing an Express server that dynamically loads routes from a specified directory.
5
- */
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
6
10
  import fs from "fs";
7
11
  import path from "path";
8
12
  import express from "express";
@@ -21,30 +25,35 @@ export class Server {
21
25
  }
22
26
  /**
23
27
  * Loads all routes from the specified directory.
28
+ * @param baseDir The base directory to resolve the routes directory from. Defaults to process.cwd().
24
29
  * @returns Promise that resolves when all routes are loaded.
25
30
  */
26
- async loadRoutes() {
27
- const routes = path.join(process.cwd(), this.routesDirectory);
28
- if (!fs.existsSync(routes)) {
29
- console.error(`Routes directory "${routes}" does not exist.`);
30
- return;
31
- }
32
- const files = fs.readdirSync(routes);
33
- await Promise.all(files.map(async (file) => {
34
- const modulePath = path.join(routes, file);
35
- const module = await import(modulePath);
36
- const RouteClass = module.default;
37
- // Pass authMiddleware to Route constructor
38
- const routeInstance = new RouteClass(this.authMiddleware);
39
- this.app.use(routeInstance.router);
40
- }));
31
+ loadRoutes() {
32
+ return __awaiter(this, arguments, void 0, function* (baseDir = process.cwd()) {
33
+ const routes = path.join(baseDir, this.routesDirectory);
34
+ if (!fs.existsSync(routes)) {
35
+ console.error(`Routes directory "${routes}" does not exist.`);
36
+ return;
37
+ }
38
+ const files = fs.readdirSync(routes);
39
+ yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () {
40
+ const modulePath = path.join(routes, file);
41
+ const module = yield import(modulePath);
42
+ const RouteClass = module.default;
43
+ // Pass authMiddleware to Route constructor
44
+ const routeInstance = new RouteClass(this.authMiddleware);
45
+ this.app.use(routeInstance.router);
46
+ })));
47
+ });
41
48
  }
42
49
  /**
43
50
  * Adds middleware functions to the Express application.
44
51
  * @param handlers - Array of Express request handlers (middleware functions).
45
52
  */
46
- async use(...handlers) {
47
- this.app.use(handlers);
53
+ use(...handlers) {
54
+ return __awaiter(this, void 0, void 0, function* () {
55
+ this.app.use(handlers);
56
+ });
48
57
  }
49
58
  /**
50
59
  * Gets the Express application instance.
@@ -55,10 +64,22 @@ export class Server {
55
64
  }
56
65
  /**
57
66
  * Starts the Express server.
67
+ * @param before Optional callback invoked before the server starts.
68
+ * @param after Optional callback invoked after the server has started.
58
69
  */
59
- start() {
60
- this.app.listen(this.port, () => {
61
- console.log(`⚡ Server running on port: ${this.port}`);
70
+ start(before, after) {
71
+ return __awaiter(this, void 0, void 0, function* () {
72
+ if (before) {
73
+ yield before();
74
+ }
75
+ yield new Promise((resolve) => {
76
+ this.app.listen(this.port, () => {
77
+ resolve();
78
+ });
79
+ });
80
+ if (after) {
81
+ yield after();
82
+ }
62
83
  });
63
84
  }
64
85
  setAuthMiddleware(middleware) {
package/eslint.config.mts CHANGED
@@ -48,28 +48,11 @@ export default defineConfig([
48
48
  }
49
49
  ],
50
50
  "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],
51
- "capitalized-comments": ["error", "always", { ignoreConsecutiveComments: true }],
52
51
  eqeqeq: ["error", "always"],
53
- "no-console": "warn",
54
- curly: ["error", "all"],
55
- "no-restricted-imports": [
56
- "error",
57
- {
58
- patterns: [".*"]
59
- }
60
- ],
61
52
  "import/order": [
62
53
  "error",
63
54
  {
64
55
  groups: ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
65
- pathGroups: [
66
- {
67
- pattern: "@/**",
68
- group: "internal",
69
- position: "before"
70
- }
71
- ],
72
- pathGroupsExcludedImportTypes: ["builtin"],
73
56
  "newlines-between": "never",
74
57
  alphabetize: {
75
58
  order: "asc",
@@ -78,11 +61,5 @@ export default defineConfig([
78
61
  }
79
62
  ]
80
63
  }
81
- },
82
- {
83
- files: ["test/**/*.{ts,mts,cts}"],
84
- rules: {
85
- "no-restricted-imports": "off"
86
- }
87
64
  }
88
65
  ]);
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "@sjoblomtobias/relay",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
7
- "build": "tsc && tsc-alias"
7
+ "build": "tsc",
8
+ "dev": "tsx --watch test/index.ts",
9
+ "lint": "eslint src",
10
+ "format": "prettier --write src"
8
11
  },
9
12
  "author": "Tobias Sjöblom",
10
- "license": "MIT",
11
- "description": "",
13
+ "license": "Pirate",
14
+ "description": "A lightweight TypeScript framework for building structured Express applications with decorator-based routing and automatic route loading.",
12
15
  "dependencies": {
13
16
  "express": "^5.1.0"
14
17
  },
@@ -25,11 +28,9 @@
25
28
  "eslint-plugin-prettier": "^5.5.4",
26
29
  "globals": "^16.4.0",
27
30
  "jiti": "^2.5.1",
28
- "nodemon": "^3.1.10",
29
31
  "prettier": "^3.6.2",
30
- "tsc-alias": "^1.8.16",
31
32
  "tsx": "^4.20.5",
32
33
  "typescript": "^5.9.2",
33
34
  "typescript-eslint": "^8.44.0"
34
35
  }
35
- }
36
+ }
Binary file
package/src/index.ts CHANGED
@@ -1,7 +1,2 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Entry point for the application.
5
- */
6
- export * from "@/route";
7
- export * from "@/server";
1
+ export * from "./route.js";
2
+ export * from "./server.js";
package/src/route.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Base class for defining routes in an Express application.
5
- */
6
1
  import { Router } from "express";
7
2
  import type { Request, Response, NextFunction } from "express";
8
3
 
package/src/server.ts CHANGED
@@ -1,9 +1,3 @@
1
- /**
2
- * @author Tobias Sjöblom
3
- * @since 1.0.0
4
- * @summary Class representing an Express server that dynamically loads routes from a specified directory.
5
- */
6
-
7
1
  import fs from "fs";
8
2
  import path from "path";
9
3
  import express from "express";
@@ -30,13 +24,13 @@ export class Server {
30
24
 
31
25
  /**
32
26
  * Loads all routes from the specified directory.
27
+ * @param baseDir The base directory to resolve the routes directory from. Defaults to process.cwd().
33
28
  * @returns Promise that resolves when all routes are loaded.
34
29
  */
35
- public async loadRoutes(): Promise<void> {
36
- const routes = path.join(process.cwd(), this.routesDirectory);
30
+ public async loadRoutes(baseDir: string = process.cwd()): Promise<void> {
31
+ const routes = path.join(baseDir, this.routesDirectory);
37
32
  if (!fs.existsSync(routes)) {
38
- console.error(`Routes directory "${routes}" does not exist.`);
39
- return;
33
+ throw new Error(`Routes directory "${routes}" does not exist.`);
40
34
  }
41
35
  const files = fs.readdirSync(routes);
42
36
  await Promise.all(
@@ -70,11 +64,21 @@ export class Server {
70
64
 
71
65
  /**
72
66
  * Starts the Express server.
67
+ * @param before Optional callback invoked before the server starts.
68
+ * @param after Optional callback invoked after the server has started.
73
69
  */
74
- public start(): void {
75
- this.app.listen(this.port, () => {
76
- console.log(`⚡ Server running on port: ${this.port}`);
70
+ public async start(before?: () => void | Promise<void>, after?: () => void | Promise<void>): Promise<void> {
71
+ if (before) {
72
+ await before();
73
+ }
74
+ await new Promise<void>((resolve) => {
75
+ this.app.listen(this.port, () => {
76
+ resolve();
77
+ });
77
78
  });
79
+ if (after) {
80
+ await after();
81
+ }
78
82
  }
79
83
 
80
84
  public setAuthMiddleware(middleware: RequestHandler) {
package/test/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { Server } from "../src/server.js";
2
+
3
+ const server = new Server(3000, "routes");
4
+
5
+ server.start(
6
+ () => {
7
+ console.log("Before start: loading routes...");
8
+ return server.loadRoutes(import.meta.dirname);
9
+ },
10
+ () => {
11
+ console.log(`⚡ Server running on port: ${server['port']}`);
12
+ }
13
+ );
@@ -0,0 +1,9 @@
1
+ import { Public, Route } from "../../src/route.js";
2
+ import type { Request, Response } from "express";
3
+
4
+ @Public("/hello")
5
+ export default class HelloRoute extends Route {
6
+ protected get(req: Request, res: Response): void {
7
+ res.json({ message: "Hello from relay!" });
8
+ }
9
+ }
package/tsconfig.json CHANGED
@@ -1,13 +1,9 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "baseUrl": ".",
4
3
  "rootDir": "src",
5
- "paths": {
6
- "@/*": ["./src/*"]
7
- },
8
- "moduleResolution": "node",
4
+ "moduleResolution": "bundler",
9
5
  "module": "esnext",
10
- "target": "ES2020",
6
+ "target": "es6",
11
7
  "esModuleInterop": true,
12
8
  "allowSyntheticDefaultImports": true,
13
9
  "strict": true,
@@ -21,9 +17,5 @@
21
17
  "declaration": true
22
18
  },
23
19
  "include": ["src/**/*"],
24
- "exclude": ["node_modules", "dist", "src/test"],
25
- "tsc-alias": {
26
- "resolveFullPaths": true,
27
- "verbose": true
28
- }
20
+ "exclude": ["node_modules", "dist", "src/test"]
29
21
  }