langaro-api 1.2.2 → 1.2.4
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/bin/langaro-api.js +12 -2
- package/lib/cli/documentation-templates/01-architecture-overview.md +240 -0
- package/lib/cli/documentation-templates/02-crud-layer.md +504 -0
- package/lib/cli/documentation-templates/03-models.md +362 -0
- package/lib/cli/documentation-templates/04-services.md +355 -0
- package/lib/cli/documentation-templates/05-controllers.md +395 -0
- package/lib/cli/documentation-templates/06-routes.md +268 -0
- package/lib/cli/documentation-templates/07-jobs.md +361 -0
- package/lib/cli/documentation-templates/08-tasks.md +265 -0
- package/lib/cli/documentation-templates/09-middlewares.md +238 -0
- package/lib/cli/documentation-templates/10-integrations.md +332 -0
- package/lib/cli/documentation-templates/11-config-and-bootstrap.md +352 -0
- package/lib/cli/documentation-templates/12-queues.md +205 -0
- package/lib/cli/documentation-templates/13-utils.md +281 -0
- package/lib/cli/documentation-templates/14-testing.md +315 -0
- package/lib/cli/documentation-templates/15-cli-and-scaffolding.md +344 -0
- package/lib/cli/documentation-templates/SUMMARY.md +116 -0
- package/lib/cli/init.js +30 -2
- package/package.json +2 -2
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Middlewares
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
|
|
5
|
+
Middlewares intercept requests before they reach controllers. The primary middleware is the **auth middleware** which handles authentication and authorization.
|
|
6
|
+
|
|
7
|
+
**What middlewares do:**
|
|
8
|
+
- Authenticate requests (JWT, API key)
|
|
9
|
+
- Check user roles and permissions
|
|
10
|
+
- Enrich `req` with user data
|
|
11
|
+
- Gate access to routes
|
|
12
|
+
|
|
13
|
+
**What middlewares do NOT do:**
|
|
14
|
+
- Execute business logic
|
|
15
|
+
- Return final responses (except for auth failures)
|
|
16
|
+
- Access the database for business operations
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Location & Naming
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/middlewares/
|
|
24
|
+
├── auth.middleware.js # Authentication & authorization
|
|
25
|
+
├── webhooks.middleware.js # Webhook signature validation (if needed)
|
|
26
|
+
└── ...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Naming convention:** `{name}.middleware.js`
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Auth Middleware (Core)
|
|
34
|
+
|
|
35
|
+
The auth middleware is the most critical middleware. It's created by `langaro-api init` and customized per project.
|
|
36
|
+
|
|
37
|
+
### Factory Pattern
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const jwt = require('jsonwebtoken');
|
|
41
|
+
const { ApiError, StatusCodes } = require('@/utils');
|
|
42
|
+
|
|
43
|
+
/** @param {ServicesMap} services @generated-types */
|
|
44
|
+
module.exports = function (services) {
|
|
45
|
+
return function auth(permissionsRequired, authorizedRoles, companyIdRequired) {
|
|
46
|
+
return async (req, res, next) => {
|
|
47
|
+
// Authentication and authorization logic
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Structure:** Factory → returns configurator → returns Express middleware
|
|
54
|
+
|
|
55
|
+
### Authentication Strategies
|
|
56
|
+
|
|
57
|
+
**1. JWT Bearer Token:**
|
|
58
|
+
```
|
|
59
|
+
Header: Authorization: Bearer <jwt-token>
|
|
60
|
+
```
|
|
61
|
+
- Decoded with `jwt.verify(token, process.env.JWT_SECRET)`
|
|
62
|
+
- Payload contains `user_id` (and optionally `email`)
|
|
63
|
+
- Token invalidated if issued before last password change
|
|
64
|
+
|
|
65
|
+
**2. API Key:**
|
|
66
|
+
```
|
|
67
|
+
Header: api_key: <key>
|
|
68
|
+
```
|
|
69
|
+
- Looked up against the users table
|
|
70
|
+
- Useful for programmatic access / integrations
|
|
71
|
+
|
|
72
|
+
### Authorization Flow
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
1. Extract token from Authorization header OR api_key header
|
|
76
|
+
2. Validate token/key → get user
|
|
77
|
+
3. Check user.role against authorizedRoles
|
|
78
|
+
4. If permissionsRequired specified:
|
|
79
|
+
a. Load user's permissions (from users_permissions table)
|
|
80
|
+
b. For each required { subject, action }:
|
|
81
|
+
- Find matching permission in user's list
|
|
82
|
+
- Wildcard action '*' matches any action
|
|
83
|
+
5. If companyIdRequired:
|
|
84
|
+
a. Read company_id from request headers
|
|
85
|
+
b. Verify user has access to that company
|
|
86
|
+
6. Set req.user_id, req.email, req.role, req.company_id, etc.
|
|
87
|
+
7. Call next()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Request Properties Set
|
|
91
|
+
|
|
92
|
+
After successful authentication, the middleware sets:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
req.user_id // User's UUID
|
|
96
|
+
req.email // User's email
|
|
97
|
+
req.role // 'user' or 'admin'
|
|
98
|
+
req.company_id // Company ID (from header, if required)
|
|
99
|
+
req.user_permissions // Array of { subject, action }
|
|
100
|
+
req.token // Raw JWT token string
|
|
101
|
+
req.user // { id, email, segment } for Sentry
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Permission System
|
|
107
|
+
|
|
108
|
+
Permissions follow a **subject + action** pattern (Attribute-Based Access Control):
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// Permission record in database
|
|
112
|
+
{
|
|
113
|
+
user_id: 'uuid-123',
|
|
114
|
+
subject: 'invoices',
|
|
115
|
+
action: '*' // Wildcard: all actions on 'invoices'
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Or specific action
|
|
119
|
+
{
|
|
120
|
+
user_id: 'uuid-123',
|
|
121
|
+
subject: 'invoices',
|
|
122
|
+
action: 'delete' // Only delete permission
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### In Routes
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
// Require any permission on subject
|
|
130
|
+
auth([{ subject: 'invoices' }])
|
|
131
|
+
|
|
132
|
+
// Require specific action
|
|
133
|
+
auth([{ subject: 'invoices', action: 'delete' }])
|
|
134
|
+
|
|
135
|
+
// Require one of multiple permissions (OR logic between entries)
|
|
136
|
+
auth([{ subject: 'invoices' }, { subject: 'admin' }])
|
|
137
|
+
|
|
138
|
+
// No permission check, just authentication
|
|
139
|
+
auth([])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
A user with `action: '*'` on a subject automatically passes any action check for that subject.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Creating Custom Middleware
|
|
147
|
+
|
|
148
|
+
Follow the same factory pattern:
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
/** @param {ServicesMap} services @generated-types */
|
|
152
|
+
module.exports = function (services) {
|
|
153
|
+
return async (req, res, next) => {
|
|
154
|
+
try {
|
|
155
|
+
// Custom logic (e.g., rate limiting, logging, feature flags)
|
|
156
|
+
const feature = await services.FeaturesServices.getWhere(
|
|
157
|
+
'name', 'new-dashboard', { firstOnly: true }
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
req.featureFlags = { newDashboard: feature?.enabled || false };
|
|
161
|
+
next();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
next(error);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Webhook Signature Validation
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
module.exports = function (services) {
|
|
173
|
+
return async (req, res, next) => {
|
|
174
|
+
const secret = req.headers.authorization;
|
|
175
|
+
|
|
176
|
+
if (process.env.NODE_ENV === 'development') {
|
|
177
|
+
return next(); // Skip in development
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (secret !== process.env.ENDPOINT_SECRET) {
|
|
181
|
+
ApiError(StatusCodes.FORBIDDEN, 'Invalid webhook signature', undefined, next);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
next();
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Creating from Scratch
|
|
193
|
+
|
|
194
|
+
1. **Create the file:** `src/middlewares/{name}.middleware.js`
|
|
195
|
+
2. **Use the template:**
|
|
196
|
+
```javascript
|
|
197
|
+
/** @param {ServicesMap} services @generated-types */
|
|
198
|
+
module.exports = function (services) {
|
|
199
|
+
return async (req, res, next) => {
|
|
200
|
+
try {
|
|
201
|
+
// Middleware logic
|
|
202
|
+
next();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
next(error);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
```
|
|
209
|
+
3. **Apply in routes:**
|
|
210
|
+
```javascript
|
|
211
|
+
const customMiddleware = require('@/middlewares/custom.middleware')(services);
|
|
212
|
+
router.get('/', customMiddleware, controller.getAll.bind(controller));
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Anti-patterns
|
|
218
|
+
|
|
219
|
+
- **Do NOT put business logic in middleware.** Keep middleware focused on cross-cutting concerns.
|
|
220
|
+
- **Do NOT forget to call `next()`** — missing `next()` hangs the request.
|
|
221
|
+
- **Do NOT catch errors without re-throwing or calling `next(error)`.**
|
|
222
|
+
- **Do NOT duplicate auth logic.** Use the auth middleware factory, don't create parallel auth checks.
|
|
223
|
+
- **Do NOT access `req.body` for auth decisions.** Auth should use headers only.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Checklist
|
|
228
|
+
|
|
229
|
+
When creating or modifying middleware:
|
|
230
|
+
|
|
231
|
+
- [ ] File is named `{name}.middleware.js`
|
|
232
|
+
- [ ] File is in `src/middlewares/`
|
|
233
|
+
- [ ] Exports a factory function `(services) => middleware`
|
|
234
|
+
- [ ] Has correct `@generated-types` JSDoc annotation
|
|
235
|
+
- [ ] Always calls `next()` or `next(error)`
|
|
236
|
+
- [ ] Error paths use `ApiError()` with appropriate status codes
|
|
237
|
+
- [ ] Does not contain business logic
|
|
238
|
+
- [ ] Auth middleware correctly validates both JWT and API key paths
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# Integrations
|
|
2
|
+
|
|
3
|
+
## Role
|
|
4
|
+
|
|
5
|
+
Integrations are **wrappers around external services**. They abstract third-party APIs (Stripe, AWS, Postmark, Google, etc.) into clean internal interfaces.
|
|
6
|
+
|
|
7
|
+
**What integrations do:**
|
|
8
|
+
- Encapsulate external API calls
|
|
9
|
+
- Normalize error handling
|
|
10
|
+
- Manage API credentials via environment variables
|
|
11
|
+
- Provide reusable methods for common operations
|
|
12
|
+
|
|
13
|
+
**What integrations do NOT do:**
|
|
14
|
+
- Contain business logic (that's services)
|
|
15
|
+
- Handle HTTP requests (that's controllers)
|
|
16
|
+
- Get auto-loaded by the framework (they're manually imported)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Location & Naming
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
src/integrations/
|
|
24
|
+
├── stripe.js # Payment processor
|
|
25
|
+
├── aws.js # S3 file storage
|
|
26
|
+
├── google.js # OAuth integration
|
|
27
|
+
├── cnpj.js # External data lookup
|
|
28
|
+
├── openai.js # AI API
|
|
29
|
+
├── postmark/ # Complex integration (directory)
|
|
30
|
+
│ ├── index.js # Main Postmark class
|
|
31
|
+
│ └── templates/ # Email templates
|
|
32
|
+
│ ├── welcome.html
|
|
33
|
+
│ └── invoice.html
|
|
34
|
+
├── focus-nfe/ # Another complex integration
|
|
35
|
+
│ ├── index.js
|
|
36
|
+
│ └── schemas/
|
|
37
|
+
└── apps/ # Webhook handlers for multiple platforms
|
|
38
|
+
├── webhooks/
|
|
39
|
+
│ ├── stripe.js
|
|
40
|
+
│ ├── shopify.js
|
|
41
|
+
│ └── ...
|
|
42
|
+
└── schemas/
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Naming convention:** `{service-name}.js` for simple integrations, `{service-name}/index.js` for complex ones with related files.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Standard Patterns
|
|
50
|
+
|
|
51
|
+
### Class-Based Integration (most common)
|
|
52
|
+
|
|
53
|
+
```javascript
|
|
54
|
+
const axios = require('axios');
|
|
55
|
+
|
|
56
|
+
class StripeIntegration {
|
|
57
|
+
constructor() {
|
|
58
|
+
this.stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async createCustomer(email, name) {
|
|
62
|
+
return this.stripe.customers.create({ email, name });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async createPaymentIntent(amount, currency = 'brl') {
|
|
66
|
+
return this.stripe.paymentIntents.create({
|
|
67
|
+
amount,
|
|
68
|
+
currency,
|
|
69
|
+
automatic_payment_methods: { enabled: true },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = new StripeIntegration();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Singleton Export
|
|
78
|
+
|
|
79
|
+
Most integrations export a singleton instance:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
module.exports = new StripeIntegration();
|
|
83
|
+
|
|
84
|
+
// Used as:
|
|
85
|
+
const stripe = require('@/integrations/stripe');
|
|
86
|
+
await stripe.createCustomer(email, name);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### AWS S3 Pattern
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
const { S3Client } = require('@aws-sdk/client-s3');
|
|
93
|
+
const { Upload } = require('@aws-sdk/lib-storage');
|
|
94
|
+
|
|
95
|
+
class AWS {
|
|
96
|
+
constructor() {
|
|
97
|
+
this.s3 = new S3Client({
|
|
98
|
+
region: process.env.AWS_REGION,
|
|
99
|
+
credentials: {
|
|
100
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
101
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async uploadToS3(bucket, { filename, file }) {
|
|
107
|
+
const upload = new Upload({
|
|
108
|
+
client: this.s3,
|
|
109
|
+
params: {
|
|
110
|
+
Bucket: bucket,
|
|
111
|
+
Key: filename,
|
|
112
|
+
Body: file,
|
|
113
|
+
ACL: 'public-read',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const { Location } = await upload.done();
|
|
118
|
+
return Location; // Returns the public URL
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async uploadToS3FromUrl(bucket, url, filename) {
|
|
122
|
+
const response = await axios.get(url, {
|
|
123
|
+
responseType: 'arraybuffer',
|
|
124
|
+
timeout: 240000,
|
|
125
|
+
});
|
|
126
|
+
return this.uploadToS3(bucket, { filename, file: response.data });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = new AWS();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Email Service Pattern (Postmark)
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
const postmark = require('postmark');
|
|
137
|
+
|
|
138
|
+
class Postmark {
|
|
139
|
+
constructor() {
|
|
140
|
+
this.client = new postmark.ServerClient(process.env.POSTMARK_API_KEY);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
filterInvalidRecipients(recipientsList) {
|
|
144
|
+
return recipientsList.filter(r => r.Email || r.email);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async sendMail(templateName, recipientsList) {
|
|
148
|
+
recipientsList = this.filterInvalidRecipients(recipientsList);
|
|
149
|
+
if (!recipientsList.length) return [];
|
|
150
|
+
|
|
151
|
+
const Messages = recipientsList.map(r => ({
|
|
152
|
+
From: 'contact@yourdomain.com',
|
|
153
|
+
To: r.Email || r.email,
|
|
154
|
+
TemplateAlias: templateName,
|
|
155
|
+
TemplateModel: r.TemplateModel || r.variables || {},
|
|
156
|
+
Attachments: r.Attachments || r.attachments || [],
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
// Chunk sending (Postmark limit: 500 per batch)
|
|
160
|
+
const chunkSize = 400;
|
|
161
|
+
const responses = [];
|
|
162
|
+
for (let i = 0; i < Messages.length; i += chunkSize) {
|
|
163
|
+
const chunk = Messages.slice(i, i + chunkSize);
|
|
164
|
+
const response = await this.client.sendEmailBatchWithTemplates(chunk);
|
|
165
|
+
responses.push(...response);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return responses;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = new Postmark();
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### External Database Connection
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
const knex = require('knex');
|
|
179
|
+
|
|
180
|
+
const externalDb = knex({
|
|
181
|
+
client: 'mysql2',
|
|
182
|
+
connection: {
|
|
183
|
+
host: process.env.EXTERNAL_DB_HOST,
|
|
184
|
+
user: process.env.EXTERNAL_DB_USER,
|
|
185
|
+
password: process.env.EXTERNAL_DB_PASSWORD,
|
|
186
|
+
database: process.env.EXTERNAL_DB_NAME,
|
|
187
|
+
port: process.env.EXTERNAL_DB_PORT,
|
|
188
|
+
},
|
|
189
|
+
pool: { min: 2, max: 10 },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
module.exports = externalDb;
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Usage in Services and Controllers
|
|
198
|
+
|
|
199
|
+
Integrations are imported directly where needed:
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
// In a service
|
|
203
|
+
const aws = require('@/integrations/aws');
|
|
204
|
+
const postmark = require('@/integrations/postmark');
|
|
205
|
+
|
|
206
|
+
module.exports = (model, models, io) => class extends model {
|
|
207
|
+
async createAndNotify(data) {
|
|
208
|
+
const record = await this.create(data);
|
|
209
|
+
|
|
210
|
+
// Upload file
|
|
211
|
+
if (data.file) {
|
|
212
|
+
const url = await aws.uploadToS3(process.env.S3_BUCKET, {
|
|
213
|
+
filename: `${record.data.id}/file.pdf`,
|
|
214
|
+
file: data.file,
|
|
215
|
+
});
|
|
216
|
+
await this.updateWhere('id', record.data.id, { file_url: url });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Send notification
|
|
220
|
+
await postmark.sendMail('record-created', [{
|
|
221
|
+
Email: data.email,
|
|
222
|
+
TemplateModel: { name: data.name },
|
|
223
|
+
}]);
|
|
224
|
+
|
|
225
|
+
return record;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Error Handling
|
|
233
|
+
|
|
234
|
+
Wrap external calls to provide clean error messages:
|
|
235
|
+
|
|
236
|
+
```javascript
|
|
237
|
+
async createCustomer(email) {
|
|
238
|
+
try {
|
|
239
|
+
return await this.stripe.customers.create({ email });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
// Log the original error for debugging
|
|
242
|
+
console.error('Stripe error:', error.message);
|
|
243
|
+
|
|
244
|
+
// Throw a clean error for the service layer
|
|
245
|
+
throw new Error(`Failed to create Stripe customer: ${error.message}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
For errors that should stop the request with a user-facing message:
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
const { ApiError, StatusCodes } = require('@/utils');
|
|
254
|
+
|
|
255
|
+
async lookupCNPJ(cnpj) {
|
|
256
|
+
try {
|
|
257
|
+
const { data } = await axios.get(`https://api.example.com/cnpj/${cnpj}`);
|
|
258
|
+
return data;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error.response?.status === 404) {
|
|
261
|
+
ApiError(StatusCodes.NOT_FOUND, 'CNPJ not found');
|
|
262
|
+
}
|
|
263
|
+
throw error; // Let other errors bubble up to Sentry
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Environment Variables
|
|
271
|
+
|
|
272
|
+
Every integration should use environment variables for credentials:
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
# .env
|
|
276
|
+
STRIPE_SECRET_KEY="sk_live_..."
|
|
277
|
+
AWS_ACCESS_KEY_ID="AKIA..."
|
|
278
|
+
AWS_SECRET_ACCESS_KEY="..."
|
|
279
|
+
AWS_REGION="us-east-1"
|
|
280
|
+
POSTMARK_API_KEY="..."
|
|
281
|
+
GOOGLE_CLIENT_ID="..."
|
|
282
|
+
GOOGLE_CLIENT_SECRET="..."
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Never hardcode API keys, URLs, or credentials in integration files.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
## Creating from Scratch
|
|
290
|
+
|
|
291
|
+
1. **Create the file:** `src/integrations/{service-name}.js`
|
|
292
|
+
2. **Define the class:**
|
|
293
|
+
```javascript
|
|
294
|
+
class ServiceName {
|
|
295
|
+
constructor() {
|
|
296
|
+
// Initialize client with env vars
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async methodName(params) {
|
|
300
|
+
// Wrap API call with error handling
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
module.exports = new ServiceName();
|
|
305
|
+
```
|
|
306
|
+
3. **Add environment variables** to `.env` and `.env.example`
|
|
307
|
+
4. **Import in services/controllers** as needed
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Anti-patterns
|
|
312
|
+
|
|
313
|
+
- **Do NOT hardcode credentials.** Always use environment variables.
|
|
314
|
+
- **Do NOT put business logic in integrations.** They are pure API wrappers.
|
|
315
|
+
- **Do NOT expose raw external errors to clients.** Catch and wrap with clean messages.
|
|
316
|
+
- **Do NOT create integrations that import services.** Integrations are lower-level — services import integrations, not the reverse.
|
|
317
|
+
- **Do NOT forget timeout configuration** for HTTP calls to external services.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Checklist
|
|
322
|
+
|
|
323
|
+
When creating a new integration:
|
|
324
|
+
|
|
325
|
+
- [ ] File is in `src/integrations/`
|
|
326
|
+
- [ ] Uses environment variables for all credentials
|
|
327
|
+
- [ ] Env vars documented in `.env.example`
|
|
328
|
+
- [ ] Exports a singleton instance (or class for multiple-instance use)
|
|
329
|
+
- [ ] Error handling wraps external errors cleanly
|
|
330
|
+
- [ ] HTTP calls have timeout configuration
|
|
331
|
+
- [ ] No business logic — pure API abstraction
|
|
332
|
+
- [ ] No circular dependencies with services
|