feat: projeto whatsapp-api-oficial — wrapper Meta Cloud API

API Node.js/Express que encapsula a Meta Cloud API (Graph API v21.0):
- POST /account/configure/:accountId — salva credenciais WABA no SQLite
- GET  /account/status/:accountId   — verifica credenciais via Meta API
- POST /message/send/:accountId     — envia texto, template ou mídia
- GET  /templates/:accountId        — lista templates aprovados da WABA
- GET  /webhooks/meta               — verificação de token (Meta handshake)
- POST /webhooks/meta               — recebe eventos Meta e repassa ao sys
- Dockerfile + docker-compose.yml com Traefik (apimsgoficial.neuralsys.com.br)
- Swagger em /api-docs com autenticação por x-api-key

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 16:49:24 -03:00
commit 995f211288
18 changed files with 736 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
## Aplicação
PORT=3001
API_KEY=sua_api_key_secreta_aqui
## Webhook: URL do whatsapp-sys que receberá os eventos da Meta
BASE_WEBHOOK_URL=https://comunicacao.neuralsys.com.br/webhooks/whatsapp-oficial
## Token de verificação que a Meta usa para confirmar seu webhook
## (configure o mesmo valor no Meta for Developers > Webhooks > Verify Token)
META_WEBHOOK_VERIFY_TOKEN=neuralsys_meta_verify_token
## Swagger
ENABLE_SWAGGER_ENDPOINT=TRUE
## Banco de dados local (credenciais WABA por account)
DB_PATH=./data/accounts.db
## Rate limiting
RATE_LIMIT_MAX=1000
RATE_LIMIT_WINDOW_MS=1000
+4
View File
@@ -0,0 +1,4 @@
node_modules/
.env
data/
swagger.json
+21
View File
@@ -0,0 +1,21 @@
FROM node:20-alpine
WORKDIR /usr/src/app
ENV NODE_ENV=production
RUN apk add --no-cache tini ca-certificates
COPY package*.json ./
# better-sqlite3 precisa de build tools (python/make/g++) para compilar no Alpine.
RUN apk add --no-cache --virtual .build-deps python3 make g++ && \
npm install --omit=dev --no-audit --no-fund && \
apk del .build-deps
COPY . .
EXPOSE 3001
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["npm", "start"]
+48
View File
@@ -0,0 +1,48 @@
version: '3.8'
services:
api_zdg_oficial:
image: chrishubert/whatsapp-oficial-api:latest
build:
context: .
dockerfile: Dockerfile
networks:
- comunidadezdg
volumes:
- api_zdg_oficial_data:/usr/src/app/data
environment:
- PORT=3001
- API_KEY=${API_KEY}
- BASE_WEBHOOK_URL=${BASE_WEBHOOK_URL}
- META_WEBHOOK_VERIFY_TOKEN=${META_WEBHOOK_VERIFY_TOKEN}
- ENABLE_SWAGGER_ENDPOINT=TRUE
- DB_PATH=/usr/src/app/data/accounts.db
deploy:
mode: replicated
replicas: 1
placement:
constraints:
- node.role == manager
resources:
limits:
cpus: "0.5"
memory: 256M
labels:
- traefik.enable=true
- traefik.http.routers.api_zdg_oficial.rule=Host(`apimsgoficial.neuralsys.com.br`)
- traefik.http.routers.api_zdg_oficial.entrypoints=websecure
- traefik.http.routers.api_zdg_oficial.tls.certresolver=letsencryptresolver
- traefik.http.routers.api_zdg_oficial.priority=1
- traefik.http.routers.api_zdg_oficial.service=api_zdg_oficial
- traefik.http.services.api_zdg_oficial.loadbalancer.server.port=3001
- traefik.http.services.api_zdg_oficial.loadbalancer.passHostHeader=true
volumes:
api_zdg_oficial_data:
external: true
name: api_zdg_oficial_data
networks:
comunidadezdg:
name: comunidadezdg
external: true
+28
View File
@@ -0,0 +1,28 @@
{
"name": "whatsapp-oficial-api",
"version": "1.0.0",
"description": "REST API wrapper para WhatsApp Cloud API (Meta) — CoreGuard",
"main": "server.js",
"scripts": {
"start": "node server.js",
"swagger": "node swagger.js"
},
"dependencies": {
"axios": "^1.14.0",
"better-sqlite3": "^11.3.0",
"dotenv": "^16.3.1",
"express": "^4.21.2",
"express-rate-limit": "^6.10.0",
"swagger-ui-express": "^4.6.3"
},
"devDependencies": {
"swagger-autogen": "^2.23.1"
},
"keywords": ["whatsapp", "cloud-api", "meta", "api", "rest", "express"],
"author": "CoreGuard / Neuralsys",
"license": "MIT",
"private": true,
"engines": {
"node": ">=18.0.0"
}
}
+8
View File
@@ -0,0 +1,8 @@
require('dotenv').config({ override: true })
const app = require('./src/app')
const port = process.env.PORT || 3001
app.listen(port, () => {
console.log(`WhatsApp Oficial API rodando na porta ${port}`)
})
+12
View File
@@ -0,0 +1,12 @@
const express = require('express')
const bodyParser = require('body-parser')
const { routes } = require('./routes')
const app = express()
app.disable('x-powered-by')
app.use(bodyParser.json({ limit: '20mb' }))
app.use(bodyParser.urlencoded({ limit: '20mb', extended: true }))
app.use('/', routes)
module.exports = app
+10
View File
@@ -0,0 +1,10 @@
module.exports = {
apiKey: process.env.API_KEY || '',
port: parseInt(process.env.PORT || '3001', 10),
baseWebhookUrl: process.env.BASE_WEBHOOK_URL || '',
metaWebhookVerifyToken: process.env.META_WEBHOOK_VERIFY_TOKEN || 'neuralsys_meta_verify_token',
enableSwagger: process.env.ENABLE_SWAGGER_ENDPOINT !== 'FALSE',
rateLimitMax: parseInt(process.env.RATE_LIMIT_MAX || '1000', 10),
rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '1000', 10),
dbPath: process.env.DB_PATH || './data/accounts.db',
}
+73
View File
@@ -0,0 +1,73 @@
const { upsertAccount, deleteAccount } = require('../storage')
const meta = require('../services/metaApiService')
exports.configure = async (req, res) => {
/* #swagger.tags = ['Account']
#swagger.summary = 'Configura credenciais WABA de uma conta'
#swagger.parameters['accountId'] = { in: 'path', required: true, schema: { type: 'string' } }
#swagger.requestBody = {
required: true,
content: { 'application/json': { schema: {
type: 'object',
required: ['phone_number_id', 'access_token'],
properties: {
phone_number_id: { type: 'string', example: '123456789012345' },
access_token: { type: 'string', example: 'EAAxxxxx...' },
waba_id: { type: 'string', example: '987654321098765' }
}
}}}
}
*/
const { accountId } = req.params
const { phone_number_id, access_token, waba_id } = req.body
if (!phone_number_id || !access_token) {
return res.status(400).json({
success: false,
message: 'Os campos phone_number_id e access_token são obrigatórios.',
})
}
try {
upsertAccount(accountId, { phone_number_id, access_token, waba_id })
return res.json({ success: true, message: `Conta "${accountId}" configurada com sucesso.` })
} catch (err) {
return res.status(500).json({ success: false, message: err.message })
}
}
exports.status = async (req, res) => {
/* #swagger.tags = ['Account']
#swagger.summary = 'Verifica se as credenciais da conta são válidas (chama Meta API)'
#swagger.parameters['accountId'] = { in: 'path', required: true, schema: { type: 'string' } }
*/
const account = req.account
try {
const data = await meta.verifyCredentials(account.phone_number_id, account.access_token)
return res.json({
success: true,
state: 'CONNECTED',
display_phone_number: data.display_phone_number,
verified_name: data.verified_name,
quality_rating: data.quality_rating,
})
} catch (err) {
const status = err.response?.status
const detail = err.response?.data?.error?.message || err.message
if (status === 401 || status === 403) {
return res.json({ success: false, state: 'INVALID_TOKEN', message: detail })
}
return res.json({ success: false, state: 'ERROR', message: detail })
}
}
exports.remove = (req, res) => {
/* #swagger.tags = ['Account']
#swagger.summary = 'Remove as credenciais de uma conta'
#swagger.parameters['accountId'] = { in: 'path', required: true, schema: { type: 'string' } }
*/
const { accountId } = req.params
deleteAccount(accountId)
return res.json({ success: true, message: `Conta "${accountId}" removida.` })
}
+7
View File
@@ -0,0 +1,7 @@
exports.ping = (req, res) => {
res.json({ success: true, message: 'pong' })
}
exports.health = (req, res) => {
res.json({ success: true, status: 'online', service: 'whatsapp-oficial-api' })
}
+83
View File
@@ -0,0 +1,83 @@
const meta = require('../services/metaApiService')
exports.sendMessage = async (req, res) => {
/* #swagger.tags = ['Message']
#swagger.summary = 'Envia uma mensagem (texto livre, template ou mídia)'
#swagger.parameters['accountId'] = { in: 'path', required: true, schema: { type: 'string' } }
#swagger.requestBody = {
required: true,
content: { 'application/json': { schema: {
type: 'object',
required: ['to'],
properties: {
to: { type: 'string', description: 'Número do destinatário (com DDD e código do país)', example: '5511999998888' },
type: { type: 'string', enum: ['text', 'template', 'media'], default: 'text' },
text: { type: 'string', description: 'Conteúdo da mensagem (type=text)', example: 'Olá, {{nome}}!' },
template: { type: 'object', description: 'Dados do template (type=template)', properties: {
name: { type: 'string', example: 'meu_template' },
language: { type: 'string', example: 'pt_BR' },
components: { type: 'array', items: { type: 'object' } }
}},
media_url: { type: 'string', description: 'URL pública da mídia (type=media)' },
media_type: { type: 'string', description: 'Tipo da mídia (image|document|video|audio)', example: 'image' },
caption: { type: 'string', description: 'Legenda da mídia (opcional)' }
}
}}}
}
*/
const account = req.account
const { to, type, text, template, media_url, media_type, caption } = req.body
if (!to) {
return res.status(400).json({ success: false, message: 'O campo "to" é obrigatório.' })
}
try {
let result
if (type === 'template') {
if (!template?.name) {
return res.status(400).json({
success: false,
message: 'template.name é obrigatório quando type=template.',
})
}
result = await meta.sendTemplateMessage(
account.phone_number_id,
account.access_token,
to,
template.name,
template.language || 'pt_BR',
template.components || []
)
} else if (type === 'media') {
if (!media_url || !media_type) {
return res.status(400).json({
success: false,
message: 'media_url e media_type são obrigatórios quando type=media.',
})
}
result = await meta.sendMediaMessage(
account.phone_number_id,
account.access_token,
to,
media_type,
media_url,
caption
)
} else {
const body = text || req.body.content
if (!body) {
return res.status(400).json({ success: false, message: 'O campo "text" é obrigatório.' })
}
result = await meta.sendTextMessage(account.phone_number_id, account.access_token, to, body)
}
const messageId = result?.messages?.[0]?.id || null
return res.json({ success: true, message: { id: messageId } })
} catch (err) {
const detail = err.response?.data?.error?.message || err.message
const code = err.response?.data?.error?.code
return res.status(500).json({ success: false, message: detail, error_code: code })
}
}
+24
View File
@@ -0,0 +1,24 @@
const meta = require('../services/metaApiService')
exports.list = async (req, res) => {
/* #swagger.tags = ['Templates']
#swagger.summary = 'Lista os templates aprovados da conta WABA'
#swagger.parameters['accountId'] = { in: 'path', required: true, schema: { type: 'string' } }
*/
const account = req.account
if (!account.waba_id) {
return res.status(400).json({
success: false,
message: 'waba_id não configurado para esta conta. Inclua o waba_id ao configurar via POST /account/configure.',
})
}
try {
const data = await meta.getTemplates(account.waba_id, account.access_token)
return res.json({ success: true, templates: data.data || [], paging: data.paging })
} catch (err) {
const detail = err.response?.data?.error?.message || err.message
return res.status(500).json({ success: false, message: detail })
}
}
+33
View File
@@ -0,0 +1,33 @@
const axios = require('axios')
const { metaWebhookVerifyToken, baseWebhookUrl } = require('../config')
exports.verify = (req, res) => {
/* #swagger.ignore = true */
const mode = req.query['hub.mode']
const token = req.query['hub.verify_token']
const challenge = req.query['hub.challenge']
if (mode === 'subscribe' && token === metaWebhookVerifyToken) {
console.log('Webhook Meta verificado com sucesso.')
return res.status(200).send(challenge)
}
console.warn('Verificação de webhook Meta falhou — token inválido.')
return res.status(403).json({ success: false, message: 'Token de verificação inválido.' })
}
exports.receive = async (req, res) => {
/* #swagger.ignore = true */
// Responde imediatamente — Meta considera timeout em 20s
res.status(200).json({ success: true })
if (!baseWebhookUrl) return
try {
await axios.post(baseWebhookUrl, req.body, {
headers: { 'Content-Type': 'application/json' },
timeout: 10000,
})
} catch (err) {
console.error('Erro ao repassar webhook Meta para o whatsapp-sys:', err.message)
}
}
+36
View File
@@ -0,0 +1,36 @@
const rateLimit = require('express-rate-limit')
const { apiKey, rateLimitMax, rateLimitWindowMs } = require('./config')
const { getAccount } = require('./storage')
const rateLimiter = rateLimit({
windowMs: rateLimitWindowMs,
max: rateLimitMax,
handler: (req, res) => {
res.status(429).json({ success: false, message: 'Muitas requisições — tente novamente em instantes.' })
},
})
function apikey (req, res, next) {
if (!apiKey) return next()
const provided = req.headers['x-api-key']
if (provided && provided === apiKey) return next()
return res.status(403).json({ success: false, message: 'API Key inválida.' })
}
function accountExists (req, res, next) {
const { accountId } = req.params
if (!accountId) {
return res.status(400).json({ success: false, message: 'accountId não fornecido.' })
}
const account = getAccount(accountId)
if (!account) {
return res.status(404).json({
success: false,
message: `Conta "${accountId}" não encontrada. Configure as credenciais primeiro via POST /account/configure/${accountId}.`,
})
}
req.account = account
return next()
}
module.exports = { rateLimiter, apikey, accountExists }
+139
View File
@@ -0,0 +1,139 @@
const express = require('express')
const routes = express.Router()
const middleware = require('./middleware')
const healthController = require('./controllers/healthController')
const accountController = require('./controllers/accountController')
const messageController = require('./controllers/messageController')
const templateController = require('./controllers/templateController')
const webhookController = require('./controllers/webhookController')
const { enableSwagger } = require('./config')
// Health (sem auth)
routes.get('/ping', healthController.ping)
routes.get('/health', healthController.health)
// Webhook Meta — sem autenticação (Meta não envia x-api-key)
routes.get('/webhooks/meta', webhookController.verify)
routes.post('/webhooks/meta', webhookController.receive)
// Valida API key para o Swagger (retorna 200 se ok)
routes.get('/validateApiKey', middleware.apikey, (req, res) => {
/* #swagger.ignore = true */
res.json({ success: true })
})
// Account management
const accountRouter = express.Router()
accountRouter.use(middleware.rateLimiter)
accountRouter.use(middleware.apikey)
routes.use('/account', accountRouter)
accountRouter.post('/configure/:accountId', accountController.configure)
accountRouter.get('/status/:accountId', [middleware.accountExists], accountController.status)
accountRouter.delete('/remove/:accountId', accountController.remove)
// Messages
const messageRouter = express.Router()
messageRouter.use(middleware.rateLimiter)
messageRouter.use(middleware.apikey)
routes.use('/message', messageRouter)
messageRouter.post('/send/:accountId', [middleware.accountExists], messageController.sendMessage)
// Templates
const templateRouter = express.Router()
templateRouter.use(middleware.rateLimiter)
templateRouter.use(middleware.apikey)
routes.use('/templates', templateRouter)
templateRouter.get('/:accountId', [middleware.accountExists], templateController.list)
// Swagger
if (enableSwagger) {
const swaggerUi = require('swagger-ui-express')
const path = require('path')
let swaggerDocument
try {
swaggerDocument = require('../swagger.json')
} catch {
swaggerDocument = {
openapi: '3.0.0',
info: { title: 'WhatsApp Oficial API', version: '1.0.0', description: 'Execute swagger.js para gerar a documentação completa.' },
paths: {},
}
}
const customCss = `
.swagger-ui .topbar { background: #1a1a2e; }
.swagger-ui .topbar .topbar-wrapper .link { display: grid; grid-template-columns: 1fr; align-items: center; }
.swagger-ui .topbar .topbar-wrapper .link img { display: none; }
.swagger-ui .topbar .topbar-wrapper .link span { display: none; }
.swagger-ui .topbar .topbar-wrapper .link::before {
content: "CoreGuard • WhatsAPI Oficial";
color: #fff; font-family: sans-serif; font-weight: 700; font-size: 20px; padding: 8px 0;
}
.swagger-ui section.models, .swagger-ui .models { display: none !important; }
body:not(.neuralsys-authorized) .swagger-ui .opblock-tag { pointer-events: none; opacity: 0.55; }
body:not(.neuralsys-authorized) .swagger-ui .opblock-tag-section .no-margin { display: none !important; }
.neuralsys-footer {
background: #000; color: #fff; padding: 16px 32px; font-size: 13px;
display: flex; justify-content: space-between; font-family: sans-serif; margin-top: 40px;
}
`
const customJsStr = `
(function () {
var authorized = false, lastKey = null, validating = false;
function currentKey() {
try {
var auth = window.ui && window.ui.authSelectors.authorized();
if (!auth) return null;
var js = auth.toJS ? auth.toJS() : auth;
if (js && js.apiKeyAuth && js.apiKeyAuth.value) return js.apiKeyAuth.value;
} catch(e) {}
return null;
}
function setAuthorized(ok) {
authorized = ok;
document.body.classList.toggle('neuralsys-authorized', ok);
if (!ok) {
document.querySelectorAll('.opblock-tag-section.is-open').forEach(function(s) {
var h = s.querySelector('.opblock-tag'); if (h) h.click();
});
}
}
function validateKey(key) {
if (validating) return; validating = true;
fetch('/validateApiKey', { headers: { 'x-api-key': key } })
.then(function(r) { setAuthorized(r.status === 200); })
.catch(function() { setAuthorized(false); })
.then(function() { validating = false; });
}
function pollAuth() {
var key = currentKey();
if (key !== lastKey) { lastKey = key; if (key) validateKey(key); else setAuthorized(false); }
}
function installFooter() {
if (document.querySelector('.neuralsys-footer')) return;
var f = document.createElement('footer');
f.className = 'neuralsys-footer';
f.innerHTML = '<span>Copyright © ' + new Date().getFullYear() + ' — CoreGuard</span><span>contato@neuralsys.com.br</span>';
document.body.appendChild(f);
}
function boot() { setAuthorized(false); setInterval(pollAuth, 500); installFooter(); }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function() { setTimeout(boot, 400); });
else setTimeout(boot, 400);
})();
`
routes.use('/api-docs', swaggerUi.serve)
routes.get('/api-docs', swaggerUi.setup(swaggerDocument, {
customCss,
customJsStr,
customSiteTitle: 'WhatsApp Oficial API — Documentação',
swaggerOptions: { docExpansion: 'none', persistAuthorization: true },
}))
}
module.exports = { routes }
+91
View File
@@ -0,0 +1,91 @@
const axios = require('axios')
const META_VERSION = 'v21.0'
const BASE = `https://graph.facebook.com/${META_VERSION}`
function buildClient (accessToken) {
return axios.create({
baseURL: BASE,
timeout: 30000,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
})
}
function sanitizePhone (phone) {
let digits = String(phone).replace(/\D+/g, '')
if (!digits.startsWith('55') && digits.length <= 11) {
digits = '55' + digits
}
return digits
}
async function sendTextMessage (phoneNumberId, accessToken, to, text) {
const client = buildClient(accessToken)
const { data } = await client.post(`/${phoneNumberId}/messages`, {
messaging_product: 'whatsapp',
to: sanitizePhone(to),
type: 'text',
text: { body: text, preview_url: false },
})
return data
}
async function sendTemplateMessage (phoneNumberId, accessToken, to, templateName, languageCode, components) {
const client = buildClient(accessToken)
const { data } = await client.post(`/${phoneNumberId}/messages`, {
messaging_product: 'whatsapp',
to: sanitizePhone(to),
type: 'template',
template: {
name: templateName,
language: { code: languageCode || 'pt_BR' },
components: components || [],
},
})
return data
}
async function sendMediaMessage (phoneNumberId, accessToken, to, mediaType, mediaUrl, caption) {
const client = buildClient(accessToken)
const mediaObj = { link: mediaUrl }
if (caption) mediaObj.caption = caption
const { data } = await client.post(`/${phoneNumberId}/messages`, {
messaging_product: 'whatsapp',
to: sanitizePhone(to),
type: mediaType,
[mediaType]: mediaObj,
})
return data
}
async function getTemplates (wabaId, accessToken) {
const client = buildClient(accessToken)
const { data } = await client.get(`/${wabaId}/message_templates`, {
params: {
limit: 100,
fields: 'name,status,language,category,components',
},
})
return data
}
async function verifyCredentials (phoneNumberId, accessToken) {
const client = buildClient(accessToken)
const { data } = await client.get(`/${phoneNumberId}`, {
params: { fields: 'display_phone_number,verified_name,quality_rating' },
timeout: 10000,
})
return data
}
module.exports = {
sendTextMessage,
sendTemplateMessage,
sendMediaMessage,
getTemplates,
verifyCredentials,
}
+42
View File
@@ -0,0 +1,42 @@
const Database = require('better-sqlite3')
const path = require('path')
const fs = require('fs')
const { dbPath } = require('./config')
const dir = path.dirname(path.resolve(dbPath))
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
const db = new Database(path.resolve(dbPath))
db.exec(`
CREATE TABLE IF NOT EXISTS accounts (
account_id TEXT PRIMARY KEY,
phone_number_id TEXT NOT NULL,
access_token TEXT NOT NULL,
waba_id TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
)
`)
function upsertAccount (accountId, data) {
db.prepare(`
INSERT INTO accounts (account_id, phone_number_id, access_token, waba_id, updated_at)
VALUES (?, ?, ?, ?, datetime('now'))
ON CONFLICT(account_id) DO UPDATE SET
phone_number_id = excluded.phone_number_id,
access_token = excluded.access_token,
waba_id = excluded.waba_id,
updated_at = excluded.updated_at
`).run(accountId, data.phone_number_id, data.access_token, data.waba_id || null)
}
function getAccount (accountId) {
return db.prepare('SELECT * FROM accounts WHERE account_id = ?').get(accountId) || null
}
function deleteAccount (accountId) {
return db.prepare('DELETE FROM accounts WHERE account_id = ?').run(accountId)
}
module.exports = { upsertAccount, getAccount, deleteAccount }
+57
View File
@@ -0,0 +1,57 @@
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0', autoBody: false })
const outputFile = './swagger.json'
const endpointsFiles = ['./src/routes.js']
const doc = {
info: {
title: 'WhatsApp Oficial API',
description: 'API REST para envio de mensagens via WhatsApp Cloud API (Meta). Suporta texto livre (janela 24h), templates aprovados e mídia.',
version: '1.0.0',
},
host: '',
securityDefinitions: {
apiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'x-api-key',
},
},
produces: ['application/json'],
tags: [
{
name: 'Account',
description: 'Configuração e verificação de credenciais WABA (phone_number_id, access_token)',
},
{
name: 'Message',
description: 'Envio de mensagens — texto livre (janela 24h), template aprovado ou mídia',
},
{
name: 'Templates',
description: 'Listagem de templates aprovados na conta WABA',
},
],
definitions: {
ConfigureAccountRequest: {
phone_number_id: '123456789012345',
access_token: 'EAAxxxxx...',
waba_id: '987654321098765',
},
SendMessageRequest: {
to: '5511999998888',
type: 'text',
text: 'Olá, mundo!',
},
SuccessResponse: {
success: true,
message: 'Operação realizada com sucesso.',
},
ErrorResponse: {
success: false,
message: 'Descrição do erro.',
},
},
}
swaggerAutogen(outputFile, endpointsFiles, doc)