yinzerflow 0.2.4 → 0.2.6

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.
@@ -0,0 +1,178 @@
1
+ # Getting Started with YinzerFlow
2
+
3
+ YinzerFlow is a lightweight, modular HTTP server framework for Node.js built with TypeScript. It provides a powerful routing system, comprehensive security features, and built-in Pittsburgh personality with flexible logging and configuration options.
4
+
5
+ ## What is YinzerFlow?
6
+
7
+ YinzerFlow is designed for developers who want:
8
+ - **Security-first approach** with built-in protections against common web vulnerabilities
9
+ - **TypeScript-first development** with full type safety and IntelliSense support
10
+ - **Flexible configuration** for different deployment environments and use cases
11
+ - **Pittsburgh personality** with witty logging and error messages
12
+ - **Modular architecture** that scales from simple APIs to complex microservices
13
+
14
+ ## Quick Start
15
+
16
+ ### Installation
17
+
18
+ ```bash
19
+ # Using npm
20
+ npm install yinzerflow
21
+
22
+ # Using Yarn
23
+ yarn add yinzerflow
24
+
25
+ # Using Bun
26
+ bun add yinzerflow
27
+ ```
28
+
29
+ ### Minimal Example
30
+
31
+ ```typescript
32
+ import { YinzerFlow } from 'yinzerflow';
33
+
34
+ // Create a new YinzerFlow instance
35
+ const app = new YinzerFlow({ port: 3000 });
36
+
37
+ // Add a simple route
38
+ app.get('/hello', () => {
39
+ return { message: 'Hello, World!' };
40
+ });
41
+
42
+ // Start the server
43
+ await app.listen();
44
+ // Although you can log the server is started, the built in logging will log this for you already when the server is listening
45
+ ```
46
+
47
+ ### Basic API Example
48
+
49
+ ```typescript
50
+ import { YinzerFlow } from 'yinzerflow';
51
+
52
+ const app = new YinzerFlow({ port: 3000 });
53
+
54
+ // GET route with parameters
55
+ app.get('/api/users/:id', ({ request }) => {
56
+ const userId = request.params.id;
57
+ const includeProfile = request.query.include_profile;
58
+
59
+ return {
60
+ id: userId,
61
+ name: 'John Doe',
62
+ includeProfile: !!includeProfile
63
+ };
64
+ });
65
+
66
+ // POST route with body parsing
67
+ app.post('/api/users', ({ request }) => {
68
+ const userData = request.body;
69
+
70
+ return {
71
+ message: 'User created successfully',
72
+ data: userData
73
+ };
74
+ });
75
+
76
+ await app.listen();
77
+ ```
78
+
79
+ ### Graceful Shutdown
80
+
81
+ YinzerFlow automatically handles graceful shutdown for SIGTERM and SIGINT signals. No manual setup required:
82
+
83
+ ```typescript
84
+ import { YinzerFlow } from 'yinzerflow';
85
+
86
+ const app = new YinzerFlow({ port: 3000 });
87
+
88
+ // Add your routes
89
+ app.get('/hello', () => {
90
+ return { message: 'Hello, World!' };
91
+ });
92
+
93
+ // Start the server - graceful shutdown is automatically configured
94
+ await app.listen();
95
+ ```
96
+
97
+ **That's it!** YinzerFlow will automatically:
98
+ - Listen for SIGTERM and SIGINT signals
99
+ - Log the shutdown process with Pittsburgh personality
100
+ - Close the server gracefully
101
+ - Exit the process cleanly
102
+
103
+ For custom shutdown handling, see [Advanced Configuration](./advanced-configuration-options.md).
104
+
105
+ ## Documentation Overview
106
+
107
+ ### Core Features
108
+
109
+ - **[Routes](./routes.md)** - Comprehensive routing system with HTTP methods, parameters, hooks, and groups
110
+ - **[Request Object](./request.md)** - Access headers, body, query parameters, route parameters, and raw body
111
+ - **[Response Object](./response.md)** - Set status codes, headers, and return various response types
112
+ - **[Body Parsing](./body-parsing.md)** - Secure parsing of JSON, file uploads, and form data with DoS protection
113
+
114
+ ### Security & Configuration
115
+
116
+ - **[Advanced Configuration](./advanced-configuration-options.md)** - Fine-tune security, performance, and functionality
117
+ - **[IP Security](./ip-security.md)** - Client IP validation and spoofing protection for load balancers and CDNs
118
+ - **[CORS](./cors.md)** - Cross-Origin Resource Sharing with comprehensive security measures
119
+ - **[Logging](./logging.md)** - Flexible logging with custom logger support and Pittsburgh personality
120
+
121
+ ### Common Use Cases
122
+
123
+ - **API Development**: Create RESTful APIs with automatic body parsing and security
124
+ - **File Uploads**: Handle multipart form data with size limits and type validation
125
+ - **Authentication**: Implement middleware hooks for token validation and user sessions
126
+ - **Rate Limiting**: Add before hooks to limit request frequency and prevent abuse
127
+ - **Load Balancer Integration**: Configure trusted proxies for accurate client IP detection
128
+ - **Production Monitoring**: Use custom loggers with structured logging and monitoring
129
+
130
+ ## Key Features
131
+
132
+ ### 🛡️ Security First
133
+ - Built-in protection against DoS attacks, prototype pollution, and memory exhaustion
134
+ - Automatic security headers and CORS validation
135
+ - IP spoofing protection with trusted proxy validation
136
+ - Comprehensive input validation and sanitization
137
+
138
+ ### 🔧 Flexible Configuration
139
+ - Configurable body parsing limits and security settings
140
+ - Custom logger integration (Winston, Pino, etc.)
141
+ - Environment-specific configurations (development, production, high-security)
142
+ - Graceful shutdown with connection timeout handling
143
+
144
+ ### 🚀 TypeScript Native
145
+ - Full TypeScript support with comprehensive type definitions
146
+ - IntelliSense support for all framework features
147
+ - Type-safe request/response handling
148
+ - Generic support for custom body and response types
149
+
150
+ ### 🎭 Pittsburgh Personality
151
+ - Witty logging messages and error handling
152
+ - Built-in Pittsburgh-themed personality
153
+ - Customizable logging with familiar `log.info()` interface
154
+ - Network request logging with nginx-style formatting
155
+
156
+ ## Next Steps
157
+
158
+ 1. **Start with Routes**: Learn the routing system in [routes.md](./routes.md)
159
+ 2. **Understand Requests**: Explore request handling in [request.md](./request.md)
160
+ 3. **Configure Security**: Set up IP security and CORS in [ip-security.md](./ip-security.md) and [cors.md](./cors.md)
161
+ 4. **Customize Logging**: Implement custom loggers in [logging.md](./logging.md)
162
+ 5. **Advanced Configuration**: Fine-tune settings in [advanced-configuration-options.md](./advanced-configuration-options.md)
163
+
164
+ ## Examples
165
+
166
+ Check out the `/example` directory for complete working examples:
167
+
168
+ - **TypeScript Example** - Full-featured API with authentication, file uploads, and custom logging
169
+ - **Basic Example** - Minimal server setup for quick prototyping
170
+ - **Production Example** - High-security configuration with monitoring and graceful shutdown
171
+
172
+ ## Contributing
173
+
174
+ We welcome contributions! Please see our contribution guidelines and feel free to submit pull requests for documentation improvements, bug fixes, or new features.
175
+
176
+ ---
177
+
178
+ **Ready to build something amazing?** Start with the [Routes documentation](./routes.md) to learn how to create your first API endpoints!
@@ -0,0 +1,11 @@
1
+ For the types to work, you need to connect to the docker container using the IDE
2
+
3
+ 1. Ensure the [Dev Containers](https://marketplace.cursorapi.com/items?itemName=ms-vscode-remote.remote-containers) extension is installed in IDE
4
+ 2. Start this image using `docker compose up -d`
5
+ 3. To start the test server run `bun start`
6
+ 4. Use `Ctrl + Shift + P`
7
+ 5. Select "Dev Containers: Attach to Running Container ..."
8
+ 6. Select this container which should be something like `yinzerflow-testing-app...`
9
+ 7. In the IDE that just opened, select folder `/app/yinzerflow-local`
10
+
11
+ To aid in development, you can use the `bun run build:watch` command in the yinzerflow (the actual module) terminal
@@ -0,0 +1,30 @@
1
+
2
+ import type { HandlerCallback } from 'yinzerflow';
3
+
4
+
5
+
6
+
7
+ export const exampleHandler: HandlerCallback<{
8
+ body: {
9
+ something: string;
10
+ };
11
+ response: {
12
+ message: string;
13
+ success: boolean;
14
+ data: {
15
+ something: string;
16
+ }
17
+ }
18
+
19
+ }> = async ({ request }) => {
20
+ const { something } = request.body;
21
+
22
+
23
+ return {
24
+ success: true,
25
+ message: "Hello World",
26
+ data: {
27
+ something
28
+ }
29
+ };
30
+ };
@@ -0,0 +1,110 @@
1
+ import { registerExampleRoutes } from "@app/routes/example.ts";
2
+ import log from "app/util/customLogger.ts";
3
+ import { YinzerFlow, type HandlerCallback } from "yinzerflow";
4
+
5
+ const app = new YinzerFlow({
6
+ cors: {
7
+ enabled: true,
8
+ origin: "*",
9
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
10
+ allowedHeaders: ["Content-Type", "Authorization", "x-session-id"],
11
+ preflightContinue: false,
12
+ },
13
+ bodyParser: {
14
+ json: {
15
+ maxDepth: 500
16
+ },
17
+
18
+ },
19
+ logLevel: "info",
20
+ networkLogs: true,
21
+ // logger: log,
22
+ // networkLogger: log,
23
+ });
24
+
25
+
26
+
27
+ app.onError((ctx) => {
28
+ ctx.response.setStatusCode(404);
29
+ return {
30
+ success: false,
31
+ message: "Something went wrong",
32
+ };
33
+ });
34
+
35
+ app.onNotFound((ctx) => {
36
+ ctx.response.setStatusCode(404);
37
+ return {
38
+ success: false,
39
+ message: "Not Found",
40
+ };
41
+ });
42
+
43
+ app.beforeAll([
44
+ (_) => {
45
+ console.log("======== beforeAll ========");
46
+ },
47
+ ]);
48
+
49
+ app.afterAll([
50
+ (ctx) => {
51
+ ctx.response.setStatusCode(202);
52
+ console.log("afterAll");
53
+ },
54
+ ]);
55
+
56
+ const login: HandlerCallback<{
57
+ response: {
58
+ success: boolean;
59
+ message: string;
60
+ };
61
+ body: {
62
+ email: string;
63
+ password: string;
64
+ };
65
+ }> = (({ request }) => {
66
+ const { email, password } = request.body;
67
+ console.log("========================");
68
+ console.log(email, password);
69
+ console.log("========================");
70
+
71
+ return {
72
+ success: true,
73
+ message: "Login successful",
74
+ };
75
+ })
76
+
77
+ app.post("/login", login);
78
+
79
+ app.group("/api", (app) => {
80
+ app.post(
81
+ "/get/:id",
82
+ ((ctx) => {
83
+ console.log("route handler");
84
+ console.log(ctx.request);
85
+
86
+ return {
87
+ message: false
88
+ };
89
+ }),
90
+ {
91
+ beforeHooks: [
92
+ (ctx) => {
93
+ ctx.response.setStatusCode(200);
94
+ console.log("beforeRoute");
95
+ },
96
+ ],
97
+ afterHooks: [
98
+ () => {
99
+ console.log("afterRoute");
100
+ },
101
+ ],
102
+ }
103
+ );
104
+ });
105
+
106
+ // Register example routes
107
+ registerExampleRoutes(app);
108
+
109
+ // Start the server AFTER defining all routes
110
+ await app.listen();
@@ -0,0 +1,18 @@
1
+ import { exampleHandler } from '@app/handlers/example.ts';
2
+ import type { YinzerFlow } from 'yinzerflow';
3
+
4
+ /**
5
+ * Register example routes on the main app instance
6
+ */
7
+ export const registerExampleRoutes = (app: YinzerFlow) => {
8
+ /**
9
+ * Get all users
10
+ */
11
+ app.post('/get-users', exampleHandler);
12
+
13
+ /**
14
+ * Add an override reason for a user to be able to add a payout method
15
+ * This is used to remove all requirements bypassing any checks such as the $20 minimum balance
16
+ */
17
+ // app.post('/user/update', updateUserController);
18
+ };
@@ -0,0 +1,166 @@
1
+
2
+ import type { TransformableInfo } from 'logform';
3
+ import { addColors, createLogger, format, transports } from 'winston';
4
+
5
+ /**
6
+ * Logging levels in winston conform to the severity ordering specified by RFC5424: severity of all levels is assumed to
7
+ * be numerically ascending from most important to least important.
8
+ */
9
+ enum LogLevel {
10
+ CRITICAL = 0,
11
+ ERROR = 1,
12
+ WARN = 2,
13
+ INFO = 3,
14
+ }
15
+
16
+ enum LogColor {
17
+ CRITICAL = 'bold white redBG',
18
+ ERROR = 'bold red',
19
+ WARN = 'bold yellow',
20
+ INFO = 'bold cyan',
21
+ }
22
+
23
+ interface Transports {
24
+ console: transports.ConsoleTransportInstance;
25
+ file: transports.FileTransportInstance;
26
+ }
27
+
28
+ /**
29
+ * Define the levels that winston will use. This is used later on
30
+ * when we create the logger
31
+ */
32
+ const logLevels = {
33
+ critical: LogLevel.CRITICAL,
34
+ error: LogLevel.ERROR,
35
+ warn: LogLevel.WARN,
36
+ info: LogLevel.INFO,
37
+ };
38
+
39
+
40
+
41
+ /**
42
+ * This lets us add custom logging levels and colors so that
43
+ * winston recognizes them
44
+ */
45
+ addColors({
46
+ critical: LogColor.CRITICAL,
47
+ error: LogColor.ERROR,
48
+ warn: LogColor.WARN,
49
+ info: LogColor.INFO,
50
+ });
51
+
52
+ /**
53
+ * Safely formats a message for logging, handling different types appropriately
54
+ */
55
+ const formatLogMessage = (message: unknown, meta: Record<string, unknown> = {}): string => {
56
+ let formattedMessage = '';
57
+
58
+ if (typeof message === 'string') {
59
+ formattedMessage = message;
60
+ } else if (typeof message === 'object' && message !== null) {
61
+ formattedMessage = JSON.stringify(message);
62
+ } else {
63
+ formattedMessage = String(message);
64
+ }
65
+
66
+ const metaKeys = Object.keys(meta);
67
+ if (metaKeys.length === 0) {
68
+ return formattedMessage;
69
+ }
70
+
71
+ const formattedMeta = JSON.stringify(meta, null, 2);
72
+
73
+ return `${formattedMessage}\n${formattedMeta}`;
74
+ };
75
+
76
+ /**
77
+ * This is the format that will be used for all logs except File logs
78
+ */
79
+ const formatter = format.combine(
80
+ /** Adds color to the format */
81
+ format.colorize(),
82
+
83
+ /** Adds timestamp to the format */
84
+ format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
85
+
86
+ /** Format the way the log is output to the console */
87
+ format.printf((info: TransformableInfo) => {
88
+ const { timestamp, level, message, ...meta } = info;
89
+ const formattedMessage = formatLogMessage(message, Object.keys(meta).length > 0 ? meta : undefined);
90
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
91
+ return `${timestamp} [${level}]: ${formattedMessage}`;
92
+ }),
93
+ );
94
+
95
+ /**
96
+ * This is the format that will be used for all File logs
97
+ * The only difference is that we don't want to add color because it will mess up the log file
98
+ */
99
+ const fileFormatter = format.combine(
100
+ /** Adds timestamp to the format */
101
+ format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
102
+
103
+ /** Format the way the log is output to the console */
104
+ format.printf((info: TransformableInfo) => {
105
+ const { timestamp, level, message, ...meta } = info;
106
+ const formattedMessage = formatLogMessage(message, Object.keys(meta).length > 0 ? meta : undefined);
107
+ // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
108
+ return `${timestamp} [${level}]: ${formattedMessage}`;
109
+ }),
110
+ );
111
+
112
+ /** ======================================== Defineing Transports ============================================ */
113
+ const transporters: Transports = {
114
+ console: new transports.Console({
115
+ format: formatter,
116
+ }),
117
+ file: new transports.File({
118
+ filename: 'storage/logs/info.log',
119
+ format: fileFormatter,
120
+ }),
121
+
122
+ };
123
+
124
+ /**
125
+ * Creates and configures the logger based on the current configuration
126
+ */
127
+ const createConfiguredLogger = (): ReturnType<typeof createLogger> => {
128
+ const transportsToUse = [];
129
+ for (const transport of ['console', 'file']) {
130
+ switch (transport) {
131
+ case 'console':
132
+ transportsToUse.push(transporters.console);
133
+ break;
134
+ case 'file':
135
+ transportsToUse.push(transporters.file);
136
+ break;
137
+ default:
138
+ transportsToUse.push(transporters.console);
139
+ break;
140
+ }
141
+ }
142
+
143
+ return createLogger({
144
+ level: 'info',
145
+ transports: transportsToUse,
146
+ levels: logLevels,
147
+ });
148
+ };
149
+
150
+ // Create the logger with the configured settings
151
+ const log = createConfiguredLogger();
152
+
153
+ export default log;
154
+ export const networkLog = createLogger({
155
+ level: 'info',
156
+ transports: [
157
+ new transports.Console({
158
+ format: formatter,
159
+ }),
160
+ new transports.File({
161
+ filename: 'storage/logs/info.log',
162
+ format: fileFormatter,
163
+ }),
164
+ ],
165
+ levels: logLevels,
166
+ });
@@ -0,0 +1,22 @@
1
+ services:
2
+ app:
3
+ user: bun
4
+ image: oven/bun:1.2.17
5
+ working_dir: /app/yinzerflow-local
6
+ volumes:
7
+ - .:/app/yinzerflow-local
8
+ - ../yinzerflow/lib:/app/yinzerflow/lib
9
+ ports:
10
+ - "5005:5000"
11
+ command: sh -c "bun install && tail -f /dev/null"
12
+ tty: true
13
+ stdin_open: true
14
+
15
+ benchmark:
16
+ image: williamyeh/wrk
17
+ restart: no
18
+ command: ["-t20", "-c400", "-d10s", "http://api:5000/status"]
19
+ depends_on:
20
+ - app
21
+ networks:
22
+ - yinzerflow-network
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "yinzerflow-local",
3
+ "type": "module",
4
+ "scripts": {
5
+ "start": "bun --watch app/index.ts"
6
+ },
7
+ "dependencies": {
8
+ "logform": "^2.7.0",
9
+ "winston": "^3.17.0",
10
+ "yinzerflow": "file:../yinzerflow/lib"
11
+ },
12
+ "devDependencies": {
13
+ "bun-types": "1.2.17",
14
+ "typescript": "^5.8.2"
15
+ }
16
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "target": "ESNext",
5
+ "useDefineForClassFields": true,
6
+ "module": "NodeNext",
7
+ "lib": ["ESNext"],
8
+ "skipLibCheck": true,
9
+ "types": ["bun-types"],
10
+
11
+ /* Bundler mode */
12
+ "moduleResolution": "NodeNext",
13
+ "moduleDetection": "force",
14
+ "allowImportingTsExtensions": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "noEmit": true,
18
+ "jsx": "preserve",
19
+ "alwaysStrict": true,
20
+ "declaration": true,
21
+ "outDir": "lib",
22
+ "declarationDir": "lib",
23
+ "verbatimModuleSyntax": true,
24
+ "baseUrl": ".",
25
+ "paths": {
26
+ "@app/*": ["./app/*"]
27
+ },
28
+ "sourceMap": false,
29
+ "removeComments": true,
30
+ "declarationMap": true,
31
+
32
+ /* Linting */
33
+ "strict": true,
34
+ "noUnusedLocals": true,
35
+ "noUnusedParameters": true,
36
+ "noFallthroughCasesInSwitch": true,
37
+ "allowSyntheticDefaultImports": true,
38
+ "allowJs": false,
39
+ "forceConsistentCasingInFileNames": true,
40
+ "noImplicitAny": true,
41
+ "noImplicitReturns": true,
42
+ "noImplicitThis": true,
43
+ "noImplicitOverride": true,
44
+ "strictFunctionTypes": true,
45
+ "strictNullChecks": true,
46
+ "strictPropertyInitialization": true,
47
+ "allowUnreachableCode": false,
48
+ "allowUnusedLabels": false,
49
+ "exactOptionalPropertyTypes": true,
50
+ "noUncheckedIndexedAccess": true,
51
+ "strictBindCallApply": true
52
+ },
53
+ "include": ["app/**/*"]
54
+ }
package/index.d.ts CHANGED
@@ -578,6 +578,12 @@ export interface InternalServerConfiguration {
578
578
  * Server connection options
579
579
  */
580
580
  connectionOptions: InternalConnectionOptions;
581
+ /**
582
+ * Automatic graceful shutdown configuration
583
+ * When enabled, YinzerFlow automatically sets up signal handlers for SIGTERM and SIGINT
584
+ * @default true
585
+ */
586
+ autoGracefulShutdown: boolean;
581
587
  }
582
588
  export interface Setup {
583
589
  get: InternalSetupMethod;
@@ -637,13 +643,13 @@ declare class SetupImpl implements InternalSetupImpl {
637
643
  readonly _routeRegistry: RouteRegistryImpl;
638
644
  readonly _hooks: HookRegistryImpl;
639
645
  constructor(customConfiguration?: ServerConfiguration);
640
- get(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
641
- head(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
642
- post(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
643
- put(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
644
- patch(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
645
- delete(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
646
- options(path: string, handler: HandlerCallback, options?: InternalRouteRegistryOptions): void;
646
+ get(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
647
+ head(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
648
+ post(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
649
+ put(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
650
+ patch(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
651
+ delete(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
652
+ options(path: string, handler: HandlerCallback<any>, options?: InternalRouteRegistryOptions): void;
647
653
  group(prefix: string, callback: (group: Record<Lowercase<InternalHttpMethod>, InternalSetupMethod>) => void, options?: InternalRouteRegistryOptions): void;
648
654
  beforeAll(handlers: Array<HandlerCallback>, options?: InternalGlobalHookOptions): void;
649
655
  afterAll(handlers: Array<HandlerCallback>, options?: InternalGlobalHookOptions): void;
@@ -664,6 +670,7 @@ export declare class YinzerFlow extends SetupImpl {
664
670
  port: number | undefined;
665
671
  host: string | undefined;
666
672
  };
673
+ private _setupGracefulShutdown;
667
674
  }
668
675
  export declare const log: {
669
676
  setLogLevel: (level: "error" | "info" | "off" | "warn") => void;