navis.js 1.0.0 → 2.0.0

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js.
4
4
 
5
+ **Author:** Syed Iman Ali
6
+
5
7
  ## Philosophy
6
8
 
7
9
  Navis.js is "Express for serverless microservices — but simpler."
@@ -71,11 +73,14 @@ exports.handler = async (event) => {
71
73
  ```bash
72
74
  # Start example server
73
75
  navis start
76
+
77
+ # Generate a new microservice (v2)
78
+ navis generate service my-service
74
79
  ```
75
80
 
76
81
  ## Features
77
82
 
78
- ### v1 (Current)
83
+ ### v1
79
84
 
80
85
  - ✅ HTTP routing (GET, POST, PUT, DELETE)
81
86
  - ✅ Middleware support (`app.use()`)
@@ -84,14 +89,20 @@ navis start
84
89
  - ✅ ServiceClient for service-to-service calls
85
90
  - ✅ Timeout support
86
91
 
87
- ### v2 (Planned)
92
+ ### v2 (Current)
93
+
94
+ - ✅ **Retry logic** - Automatic retry with exponential backoff
95
+ - ✅ **Circuit breaker** - Prevents cascading failures
96
+ - ✅ **Config-based services** - Centralized service configuration
97
+ - ✅ **Service discovery** - Health checks and load balancing
98
+ - ✅ **Additional HTTP methods** - PUT, DELETE, PATCH support
99
+ - ✅ **CLI generators** - `navis generate service` command
100
+
101
+ ### v3 (Planned)
88
102
 
89
- - 🔄 Retry logic
90
- - 🔄 Circuit breaker
91
- - 🔄 Config-based services
92
- - 🔄 Service discovery
93
103
  - 🔄 Async messaging (SQS / Kafka / NATS)
94
- - 🔄 CLI generators (`navis generate service`)
104
+ - 🔄 Advanced observability
105
+ - 🔄 Enhanced CLI features
95
106
 
96
107
  ## API Reference
97
108
 
@@ -107,19 +118,66 @@ navis start
107
118
  - `app.listen(port, callback)` - Start HTTP server (Node.js)
108
119
  - `app.handleLambda(event)` - Handle AWS Lambda event
109
120
 
110
- ### ServiceClient
121
+ ### ServiceClient (v2 Enhanced)
111
122
 
123
+ ```javascript
112
124
  const { ServiceClient } = require('navis.js');
113
125
 
126
+ // Basic usage
114
127
  const client = new ServiceClient('http://api.example.com', {
115
- timeout: 5000, // milliseconds
128
+ timeout: 5000,
129
+ });
130
+
131
+ // With retry and circuit breaker (v2)
132
+ const resilientClient = new ServiceClient('http://api.example.com', {
133
+ timeout: 5000,
134
+ maxRetries: 3,
135
+ retryBaseDelay: 1000,
136
+ circuitBreaker: {
137
+ failureThreshold: 5,
138
+ resetTimeout: 60000,
139
+ },
116
140
  });
117
141
 
118
- // GET request
119
- const response = await client.get('/users');
142
+ // All HTTP methods (v2)
143
+ await client.get('/users');
144
+ await client.post('/users', { name: 'John' });
145
+ await client.put('/users/1', { name: 'Jane' }); // v2
146
+ await client.patch('/users/1', { name: 'Bob' }); // v2
147
+ await client.delete('/users/1'); // v2
148
+ ```
149
+
150
+ ### Service Configuration (v2)
151
+
152
+ ```javascript
153
+ const { ServiceConfig, ServiceClient } = require('navis.js');
154
+
155
+ const config = new ServiceConfig({
156
+ defaultOptions: {
157
+ timeout: 5000,
158
+ retry: { maxRetries: 3 },
159
+ circuitBreaker: { failureThreshold: 5 },
160
+ },
161
+ });
120
162
 
121
- // POST request
122
- const result = await client.post('/users', { name: 'John' });
163
+ config.register('userService', 'http://localhost:3001');
164
+ const userConfig = config.get('userService');
165
+ const client = new ServiceClient(userConfig.baseUrl, userConfig);
166
+ ```
167
+
168
+ ### Service Discovery (v2)
169
+
170
+ ```javascript
171
+ const { ServiceDiscovery, ServiceClient } = require('navis.js');
172
+
173
+ const discovery = new ServiceDiscovery();
174
+ discovery.register('api', [
175
+ 'http://api1.example.com',
176
+ 'http://api2.example.com',
177
+ ]);
178
+
179
+ const url = discovery.getNext('api'); // Round-robin
180
+ const client = new ServiceClient(url);
123
181
  ```
124
182
 
125
183
  ### Response Helpers
@@ -141,18 +199,30 @@ See the `examples/` directory:
141
199
  - `server.js` - Node.js HTTP server example
142
200
  - `lambda.js` - AWS Lambda handler example
143
201
  - `service-client-demo.js` - ServiceClient usage example
202
+ - `v2-features-demo.js` - v2 features demonstration (retry, circuit breaker, etc.)
144
203
 
145
204
  ## Roadmap
146
205
 
147
- ### v1 (Current)
206
+ ### v1
148
207
  Core functionality: routing, middleware, Lambda support, ServiceClient
149
208
 
150
- ### v2 (Next)
151
- Resilience patterns: retry, circuit breaker, service discovery
209
+ ### v2 (Current)
210
+ Resilience patterns: retry, circuit breaker, service discovery, CLI generators
152
211
 
153
212
  ### v3 (Future)
154
- Advanced features: async messaging, observability, advanced CLI
213
+ Advanced features: async messaging (SQS/Kafka/NATS), observability, enhanced CLI
214
+
215
+ ## Documentation
216
+
217
+ - [V2 Features Guide](./V2_FEATURES.md) - Complete v2 features documentation
218
+ - [Verification Guide](./VERIFY_V2.md) - How to verify all features
155
219
 
156
220
  ## License
157
221
 
158
- MIT
222
+ MIT
223
+
224
+ ## Author
225
+
226
+ **Syed Iman Ali**
227
+
228
+ Created with ❤️ for the serverless microservices community.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Service Generator
3
+ * v2: Generate service boilerplate
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Generate service boilerplate
11
+ * @param {string} serviceName - Name of the service
12
+ * @param {string} targetDir - Target directory (default: current directory)
13
+ */
14
+ function generateService(serviceName, targetDir = process.cwd()) {
15
+ const serviceDir = path.join(targetDir, serviceName);
16
+
17
+ // Create service directory
18
+ if (!fs.existsSync(serviceDir)) {
19
+ fs.mkdirSync(serviceDir, { recursive: true });
20
+ }
21
+
22
+ // Generate service.js
23
+ const serviceTemplate = `const { NavisApp, response } = require('navis.js');
24
+
25
+ const app = new NavisApp();
26
+
27
+ // Middleware
28
+ app.use((req, res, next) => {
29
+ console.log(\`\${req.method} \${req.url}\`);
30
+ next();
31
+ });
32
+
33
+ // Routes
34
+ app.get('/', (req, res) => {
35
+ response.success(res, {
36
+ service: '${serviceName}',
37
+ message: 'Welcome to ${serviceName} service',
38
+ version: '1.0.0'
39
+ });
40
+ });
41
+
42
+ app.get('/health', (req, res) => {
43
+ response.success(res, { status: 'ok', service: '${serviceName}' });
44
+ });
45
+
46
+ // Add your routes here
47
+ // app.get('/api/users', (req, res) => { ... });
48
+ // app.post('/api/users', (req, res) => { ... });
49
+
50
+ // Start server
51
+ const PORT = process.env.PORT || 3000;
52
+ app.listen(PORT, () => {
53
+ console.log(\`${serviceName} service running on http://localhost:\${PORT}\`);
54
+ });
55
+
56
+ module.exports = app;
57
+ `;
58
+
59
+ fs.writeFileSync(path.join(serviceDir, 'service.js'), serviceTemplate);
60
+
61
+ // Generate lambda.js
62
+ const lambdaTemplate = `const { NavisApp } = require('navis.js');
63
+
64
+ const app = new NavisApp();
65
+
66
+ // Middleware
67
+ app.use((req, res, next) => {
68
+ console.log(\`Lambda: \${req.method} \${req.path}\`);
69
+ next();
70
+ });
71
+
72
+ // Routes
73
+ app.get('/', (req, res) => {
74
+ res.statusCode = 200;
75
+ res.body = {
76
+ service: '${serviceName}',
77
+ message: 'Welcome to ${serviceName} service (Lambda)',
78
+ version: '1.0.0'
79
+ };
80
+ });
81
+
82
+ app.get('/health', (req, res) => {
83
+ res.statusCode = 200;
84
+ res.body = { status: 'ok', service: '${serviceName}' };
85
+ });
86
+
87
+ // Lambda handler
88
+ exports.handler = async (event) => {
89
+ return await app.handleLambda(event);
90
+ };
91
+ `;
92
+
93
+ fs.writeFileSync(path.join(serviceDir, 'lambda.js'), lambdaTemplate);
94
+
95
+ // Generate package.json
96
+ const packageJson = {
97
+ name: serviceName,
98
+ version: '1.0.0',
99
+ description: `${serviceName} microservice`,
100
+ main: 'service.js',
101
+ scripts: {
102
+ start: 'node service.js',
103
+ 'start:lambda': 'node lambda.js',
104
+ },
105
+ dependencies: {
106
+ 'navis.js': '^1.0.0',
107
+ },
108
+ };
109
+
110
+ fs.writeFileSync(
111
+ path.join(serviceDir, 'package.json'),
112
+ JSON.stringify(packageJson, null, 2)
113
+ );
114
+
115
+ // Generate README.md
116
+ const readmeTemplate = `# ${serviceName}
117
+
118
+ Microservice generated with Navis.js
119
+
120
+ ## Installation
121
+
122
+ \`\`\`bash
123
+ npm install
124
+ \`\`\`
125
+
126
+ ## Running
127
+
128
+ ### Local Development
129
+
130
+ \`\`\`bash
131
+ npm start
132
+ # or
133
+ node service.js
134
+ \`\`\`
135
+
136
+ ### AWS Lambda
137
+
138
+ Deploy \`lambda.js\` to AWS Lambda and configure API Gateway.
139
+
140
+ ## API Endpoints
141
+
142
+ - \`GET /\` - Service information
143
+ - \`GET /health\` - Health check
144
+
145
+ ## Development
146
+
147
+ Add your routes in \`service.js\`:
148
+
149
+ \`\`\`javascript
150
+ app.get('/api/users', (req, res) => {
151
+ // Your route handler
152
+ });
153
+ \`\`\`
154
+ `;
155
+
156
+ fs.writeFileSync(path.join(serviceDir, 'README.md'), readmeTemplate);
157
+
158
+ console.log(`✅ Service "${serviceName}" generated successfully!`);
159
+ console.log(`📁 Location: ${serviceDir}`);
160
+ console.log(`\nNext steps:`);
161
+ console.log(` cd ${serviceName}`);
162
+ console.log(` npm install`);
163
+ console.log(` npm start`);
164
+ }
165
+
166
+ module.exports = { generateService };
167
+
package/bin/navis.js CHANGED
@@ -33,8 +33,26 @@ if (command === 'start') {
33
33
  process.exit(code);
34
34
  });
35
35
  } else if (command === 'generate') {
36
- // TODO v2: Implement navis generate service
37
- console.log('Generator commands coming in v2');
36
+ const subcommand = process.argv[3];
37
+
38
+ if (subcommand === 'service') {
39
+ const serviceName = process.argv[4];
40
+
41
+ if (!serviceName) {
42
+ console.error('Error: Service name is required');
43
+ console.log('Usage: navis generate service <service-name>');
44
+ process.exit(1);
45
+ }
46
+
47
+ const { generateService } = require('./generators/service');
48
+ generateService(serviceName);
49
+ } else {
50
+ console.log('Generator commands:');
51
+ console.log(' navis generate service <name> Generate a new microservice');
52
+ console.log('');
53
+ console.log('Example:');
54
+ console.log(' navis generate service user-service');
55
+ }
38
56
  } else {
39
57
  console.log('Navis.js CLI');
40
58
  console.log('');
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Navis.js v2 Features Demo
3
+ * Demonstrates retry logic, circuit breaker, service config, and service discovery
4
+ */
5
+
6
+ const {
7
+ ServiceClient,
8
+ ServiceConfig,
9
+ ServiceDiscovery,
10
+ retry,
11
+ } = require('../src/index');
12
+
13
+ // Example 1: ServiceClient with retry and circuit breaker
14
+ async function demoServiceClient() {
15
+ console.log('\n=== ServiceClient with Retry & Circuit Breaker ===\n');
16
+
17
+ const client = new ServiceClient('http://localhost:3000', {
18
+ timeout: 3000,
19
+ maxRetries: 3,
20
+ retryBaseDelay: 1000,
21
+ circuitBreaker: {
22
+ failureThreshold: 3,
23
+ resetTimeout: 10000,
24
+ },
25
+ });
26
+
27
+ try {
28
+ const response = await client.get('/health');
29
+ console.log('✅ Request successful:', response.data);
30
+ } catch (error) {
31
+ if (error.circuitBreakerOpen) {
32
+ console.log('❌ Circuit breaker is OPEN:', error.message);
33
+ console.log('Circuit state:', error.circuitState);
34
+ } else {
35
+ console.log('❌ Request failed:', error.message);
36
+ }
37
+ }
38
+
39
+ // Check circuit breaker state
40
+ const state = client.getCircuitBreakerState();
41
+ if (state) {
42
+ console.log('Circuit breaker state:', state);
43
+ }
44
+ }
45
+
46
+ // Example 2: Service Configuration
47
+ async function demoServiceConfig() {
48
+ console.log('\n=== Service Configuration ===\n');
49
+
50
+ const config = new ServiceConfig({
51
+ defaultOptions: {
52
+ timeout: 5000,
53
+ retry: {
54
+ maxRetries: 3,
55
+ baseDelay: 1000,
56
+ },
57
+ circuitBreaker: {
58
+ failureThreshold: 5,
59
+ resetTimeout: 60000,
60
+ },
61
+ },
62
+ });
63
+
64
+ // Register services
65
+ config.register('userService', 'http://localhost:3001', {
66
+ timeout: 3000,
67
+ });
68
+
69
+ config.register('orderService', 'http://localhost:3002', {
70
+ retry: {
71
+ maxRetries: 5,
72
+ },
73
+ });
74
+
75
+ console.log('Registered services:', Object.keys(config.getAll()));
76
+
77
+ // Get service config
78
+ const userServiceConfig = config.get('userService');
79
+ console.log('User service config:', userServiceConfig);
80
+ }
81
+
82
+ // Example 3: Service Discovery
83
+ async function demoServiceDiscovery() {
84
+ console.log('\n=== Service Discovery ===\n');
85
+
86
+ const discovery = new ServiceDiscovery({
87
+ healthCheckInterval: 10000,
88
+ healthCheckPath: '/health',
89
+ });
90
+
91
+ // Register service with multiple endpoints
92
+ discovery.register('api', [
93
+ 'http://localhost:3000',
94
+ 'http://localhost:3001',
95
+ 'http://localhost:3002',
96
+ ]);
97
+
98
+ // Get next available service (round-robin)
99
+ const url1 = discovery.getNext('api');
100
+ console.log('Next service URL:', url1);
101
+
102
+ const url2 = discovery.getNext('api');
103
+ console.log('Next service URL:', url2);
104
+
105
+ // Get all healthy services
106
+ const healthy = discovery.getHealthy('api');
107
+ console.log('Healthy services:', healthy);
108
+ }
109
+
110
+ // Example 4: Using retry utility directly
111
+ async function demoRetryUtility() {
112
+ console.log('\n=== Retry Utility ===\n');
113
+
114
+ let attemptCount = 0;
115
+
116
+ const flakyFunction = async () => {
117
+ attemptCount++;
118
+ console.log(`Attempt ${attemptCount}...`);
119
+
120
+ if (attemptCount < 3) {
121
+ throw new Error('Temporary failure');
122
+ }
123
+
124
+ return { success: true, attempt: attemptCount };
125
+ };
126
+
127
+ try {
128
+ const result = await retry(flakyFunction, {
129
+ maxRetries: 3,
130
+ baseDelay: 500,
131
+ });
132
+ console.log('✅ Success after retries:', result);
133
+ } catch (error) {
134
+ console.log('❌ Failed after all retries:', error.message);
135
+ }
136
+ }
137
+
138
+ // Example 5: ServiceClient with additional HTTP methods
139
+ async function demoHttpMethods() {
140
+ console.log('\n=== Additional HTTP Methods ===\n');
141
+
142
+ const client = new ServiceClient('http://localhost:3000', {
143
+ retry: false, // Disable retry for demo
144
+ });
145
+
146
+ try {
147
+ // PUT request
148
+ const putResponse = await client.put('/api/users/1', { name: 'John' });
149
+ console.log('PUT response:', putResponse.data);
150
+
151
+ // DELETE request
152
+ const deleteResponse = await client.delete('/api/users/1');
153
+ console.log('DELETE response:', deleteResponse.data);
154
+
155
+ // PATCH request
156
+ const patchResponse = await client.patch('/api/users/1', { name: 'Jane' });
157
+ console.log('PATCH response:', patchResponse.data);
158
+ } catch (error) {
159
+ console.log('Request failed (expected if server not running):', error.message);
160
+ }
161
+ }
162
+
163
+ // Run all demos
164
+ async function runDemos() {
165
+ console.log('🚀 Navis.js v2 Features Demo\n');
166
+ console.log('='.repeat(50));
167
+
168
+ await demoServiceConfig();
169
+ await demoServiceDiscovery();
170
+ await demoRetryUtility();
171
+ await demoHttpMethods();
172
+
173
+ // ServiceClient demo requires a running server
174
+ console.log('\n💡 Note: ServiceClient demo requires a running server on port 3000');
175
+ console.log(' Run: node examples/server.js');
176
+ }
177
+
178
+ // Run if called directly
179
+ if (require.main === module) {
180
+ runDemos().catch(console.error);
181
+ }
182
+
183
+ module.exports = {
184
+ demoServiceClient,
185
+ demoServiceConfig,
186
+ demoServiceDiscovery,
187
+ demoRetryUtility,
188
+ demoHttpMethods,
189
+ };
190
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "navis.js",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "A lightweight, serverless-first, microservice API framework designed for AWS Lambda and Node.js",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -1,12 +1,30 @@
1
- const NavisApp = require('./core/app');
2
- const ServiceClient = require('./utils/service-client');
3
- const { success, error } = require('./utils/response');
4
-
5
- module.exports = {
6
- NavisApp,
7
- ServiceClient,
8
- response: {
9
- success,
10
- error,
11
- },
12
- };
1
+ const NavisApp = require('./core/app');
2
+ const ServiceClient = require('./utils/service-client');
3
+ const ServiceConfig = require('./utils/service-config');
4
+ const ServiceDiscovery = require('./utils/service-discovery');
5
+ const CircuitBreaker = require('./utils/circuit-breaker');
6
+ const { success, error } = require('./utils/response');
7
+ const { retry, shouldRetryHttpStatus } = require('./utils/retry');
8
+
9
+ module.exports = {
10
+ // Core
11
+ NavisApp,
12
+
13
+ // Service Client (v2 enhanced)
14
+ ServiceClient,
15
+
16
+ // v2 Features
17
+ ServiceConfig,
18
+ ServiceDiscovery,
19
+ CircuitBreaker,
20
+
21
+ // Utilities
22
+ response: {
23
+ success,
24
+ error,
25
+ },
26
+ retry: {
27
+ retry,
28
+ shouldRetryHttpStatus,
29
+ },
30
+ };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Circuit Breaker - Prevents cascading failures in microservices
3
+ * v2: Circuit breaker pattern implementation
4
+ */
5
+
6
+ class CircuitBreaker {
7
+ constructor(options = {}) {
8
+ this.failureThreshold = options.failureThreshold || 5; // Open circuit after 5 failures
9
+ this.resetTimeout = options.resetTimeout || 60000; // 60 seconds
10
+ this.monitoringWindow = options.monitoringWindow || 10000; // 10 seconds
11
+
12
+ this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
13
+ this.failureCount = 0;
14
+ this.successCount = 0;
15
+ this.lastFailureTime = null;
16
+ this.nextAttemptTime = null;
17
+ }
18
+
19
+ /**
20
+ * Record a successful request
21
+ */
22
+ recordSuccess() {
23
+ if (this.state === 'HALF_OPEN') {
24
+ this.successCount++;
25
+ if (this.successCount >= 2) {
26
+ // Reset to CLOSED after 2 successes in HALF_OPEN
27
+ this.state = 'CLOSED';
28
+ this.failureCount = 0;
29
+ this.successCount = 0;
30
+ }
31
+ } else if (this.state === 'CLOSED') {
32
+ this.failureCount = 0; // Reset failure count on success
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Record a failed request
38
+ */
39
+ recordFailure() {
40
+ this.failureCount++;
41
+ this.lastFailureTime = Date.now();
42
+
43
+ if (this.state === 'CLOSED' && this.failureCount >= this.failureThreshold) {
44
+ // Open the circuit
45
+ this.state = 'OPEN';
46
+ this.nextAttemptTime = Date.now() + this.resetTimeout;
47
+ } else if (this.state === 'HALF_OPEN') {
48
+ // Failed in half-open, go back to open
49
+ this.state = 'OPEN';
50
+ this.nextAttemptTime = Date.now() + this.resetTimeout;
51
+ this.successCount = 0;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Check if request should be allowed
57
+ * @returns {boolean} - true if request should proceed
58
+ */
59
+ canAttempt() {
60
+ if (this.state === 'CLOSED') {
61
+ return true;
62
+ }
63
+
64
+ if (this.state === 'OPEN') {
65
+ if (Date.now() >= this.nextAttemptTime) {
66
+ // Transition to HALF_OPEN
67
+ this.state = 'HALF_OPEN';
68
+ this.successCount = 0;
69
+ return true;
70
+ }
71
+ return false; // Circuit is open, reject request
72
+ }
73
+
74
+ if (this.state === 'HALF_OPEN') {
75
+ return true; // Allow limited requests in half-open state
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * Get current circuit state
83
+ */
84
+ getState() {
85
+ return {
86
+ state: this.state,
87
+ failureCount: this.failureCount,
88
+ successCount: this.successCount,
89
+ lastFailureTime: this.lastFailureTime,
90
+ nextAttemptTime: this.nextAttemptTime,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Reset circuit breaker
96
+ */
97
+ reset() {
98
+ this.state = 'CLOSED';
99
+ this.failureCount = 0;
100
+ this.successCount = 0;
101
+ this.lastFailureTime = null;
102
+ this.nextAttemptTime = null;
103
+ }
104
+ }
105
+
106
+ module.exports = CircuitBreaker;
107
+