npm package: @sampathkumara/express-to-dart-endpoints-builder
Description: Automatically generate Dart API clients from Express.js route definitions with JSDoc comments
Author: Sampath Kumara
Latest version: 1.1.0

# Express to Dart Endpoints Builder

Automatically generate production-ready Dart API clients from your Express.js route definitions with full JSDoc support.

## Features

- 🚀 **Automatic code generation** - Generate Dart API client methods from Express routes
- 📚 **JSDoc support** - Extract parameter types and documentation automatically
- 🔐 **Authentication handling** - Detect and include auth checks in generated methods
- 📝 **Parameter parsing** - Automatically extract path, query, and body parameters
- 📦 **File upload/download** - Support for multipart file uploads and file downloads
- ✅ **Permission checks** - Include permission validation in generated code
- 🎯 **Route tree traversal** - Recursively discover routes across all sub-routers
- 📄 **Type mapping** - Convert JavaScript/JSDoc types to Dart types automatically
- 💾 **Configuration driven** - Fully customizable via JSON config
- 🔵 **TypeScript support** - Works with `.js`, `.ts`, and `.tsx` route files

## Installation

### As an npm package

```bash
npm install @sampathkumara/express-to-dart-endpoints-builder
```

### Global CLI

```bash
npm install -g @sampathkumara/express-to-dart-endpoints-builder
```

### Local development

```bash
git clone <repository>
cd express-to-dart-endpoints-builder
npm link
```

## Quick Start

### 1. Initialize configuration

```bash
express-to-dart init
```

This creates a default `endpoints-to-dart.config.js` in your current directory.

### 2. Configure your project

Edit `endpoints-to-dart.config.js`:

```javascript
module.exports = {
  routesDir: './routes',
  outputDir: './dart/lib/api',
  baseUrl: 'http://localhost:3000',
  apiClassName: 'Api',
  classNameEndpoints: 'ServerEndpoints',
  imports: [
    'package:your_app/api/Api.dart',
    'package:your_app/models/AppUser.dart'
  ]
};
```

### 3. Generate Dart client

```bash
express-to-dart generate
# or simply
express-to-dart
```

This generates `ServerEndpoints.dart` in your configured output directory.

## Configuration

### endpoints-to-dart.config.js

All configuration options:

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `routesDir` | string | `../routes` | Path to Express routes directory (relative to config file) |
| `outputDir` | string | `../../app/lib/api` | Output directory for generated Dart file (relative to config file) |
| `baseUrl` | string | `http://localhost:3000` | API base URL (for documentation) |
| `apiClassName` | string | `Api` | Name of the API class to call in generated methods |
| `classNameEndpoints` | string | `ServerEndpoints` | Name of the generated endpoints class |
| `imports` | array | See example | Dart import statements to include in generated file |
| `includePatterns` | array | `["**/*.js", "**/*.ts", "**/*.tsx"]` | Glob patterns for files to include |
| `excludePatterns` | array | See example | Glob patterns for files to exclude |

### Example Configuration

```json
{
  "routesDir": "./routes",
  "outputDir": "./dart/lib/api",
  "baseUrl": "http://api.example.com",
  "apiClassName": "ApiClient",
  "classNameEndpoints": "AppEndpoints",
  "imports": [
    "package:your_app/api/client.dart",
    "package:your_app/models/user.dart",
    "package:your_app/models/permissions.dart"
  ],
  "includePatterns": ["**/*.js", "**/*.ts", "**/*.tsx"],
  "excludePatterns": [
    "**/node_modules/**",
    "**/*.test.js",
    "**/*.spec.js",
    "**/*.test.ts",
    "**/*.spec.ts"
  ]
}
```

## JSDoc Documentation

### Parameter Documentation

Document parameters with `@var` tags. The generator automatically detects parameter location (path, query, body):

```javascript
/**
 * Get user by ID
 * @var {string} userId - The user ID (automatically detected as path param)
 * @var {boolean} includeDetails - Include additional user details
 * @var {object} metadata - Additional metadata to save
 */
router.get('/users/:userId', async (req, res) => {
  // ...
});
```

### Explicit Parameter Location

For clarity, explicitly specify parameter location:

```javascript
/**
 * Search users
 * @var {string} req.query.q - Search query
 * @var {number} req.query.limit - Results limit
 * @var {object} req.body - Request body object
 */
router.post('/users/search', async (req, res) => {
  // ...
});
```

### Return Type & File Downloads

Mark methods that return files:

```javascript
/**
 * Download user export as PDF
 * @var {string} userId - User ID
 * @returns {file}
 */
router.get('/users/:userId/export', async (req, res) => {
  // ...
});
```

Generated method includes:
- `savePath` parameter for file save location
- `useServerFileName` option
- Progress callback support
- Calls `Api.downloadFile()` instead of regular HTTP method

### Deprecation

Mark deprecated endpoints:

```javascript
/**
 * Get legacy user data
 * @deprecated Use getUserProfile instead
 */
router.get('/api/v1/users/:id', async (req, res) => {
  // ...
});
```

Generated method will have `@Deprecated()` annotation in Dart.

## Type Mapping

### Supported Types

| JS/JSDoc Type | Dart Type |
|---------------|-----------|
| `string` | `String` |
| `number` | `double` |
| `int`, `integer` | `int` |
| `boolean`, `bool` | `bool` |
| `date`, `datetime` | `DateTime` |
| `file` | `File` |
| `object`, `json`, `map` | `Map<String, dynamic>` |
| `array`, `list`, `[]` | `List<dynamic>` |
| `string[]`, `list<string>` | `List<String>` |
| `map<string, int>` | `Map<String, int>` |
| `any`, `*` | `dynamic` |

### Examples

```javascript
/**
 * Complex parameter example
 * @var {string[]} tags - List of tags
 * @var {object} metadata - Metadata object
 * @var {map<string, int>} scores - Score mapping
 * @var {file} document - Document file
 */
router.post('/items', async (req, res) => {
  // ...
});
```

## Generated Dart Code

### ServerEndpoints Class

All endpoint methods are static and return `Future<Response>`:

```dart
class ServerEndpoints {
  /// Get user profile by ID
  /// Source: routes/users.js:12
  /// Authenticated: true
  /// Permissions: NsPermissions.USER_READ
  static Future<Response> getUsersById({
    required String userId,
    required String includeDetails,
    Map<String, dynamic>? data,
    ApiOptions? options,
  }) async {
    if (!AppUser.havePermissionFor(NsPermissions.USER_READ)) {
      throw Exception("Permission denied: NsPermissions.USER_READ");
    }

    return Api.get(
      '/users/${userId}',
      data: {
        ...queryParams,
        ...(data ?? {}),
      },
      reFreshToken: options?.reFreshToken ?? false,
      cancelToken: options?.cancelToken,
      enableRetry: options?.enableRetry ?? true,
      skipAuth: options?.skipAuth ?? false,
      maxRetries: options?.maxRetries ?? 3,
      options: options?.options,
    );
  }
}
```

### ApiOptions Class

Control request behavior:

```dart
class ApiOptions {
  final bool reFreshToken;                               // Force token refresh
  final CancelToken? cancelToken;                        // Dio cancel token
  final void Function(int, int)? onReceiveProgress;      // Download progress
  final void Function(int, int)? onSendProgress;         // Upload progress
  final bool enableRetry;                                // Enable retry logic
  final bool? skipAuth;                                  // Bypass authentication
  final int maxRetries;                                  // Retry attempts (default 3)
  final Options? options;                                // Custom Dio options
  final bool? useServerFileName;                         // Use server filename for downloads
  final FormData? formData;                              // Custom FormData
}
```

### Usage Example

```dart
// Simple GET request
final response = await ServerEndpoints.getUsersById(
  userId: '123',
  includeDetails: true,
);

// With options and error handling
try {
  final response = await ServerEndpoints.createUser(
    name: 'John',
    email: 'john@example.com',
    options: ApiOptions(
      enableRetry: true,
      maxRetries: 5,
    ),
  );
} catch (e) {
  print('Error: $e');
}

// File download with progress
final response = await ServerEndpoints.downloadExport(
  userId: '123',
  savePath: '/path/to/save',
  useServerFileName: true,
  options: ApiOptions(
    onReceiveProgress: (received, total) {
      print('Progress: ${(received / total * 100).toStringAsFixed(0)}%');
    },
  ),
);

// File upload
final file = File('/path/to/file.pdf');
final response = await ServerEndpoints.uploadDocument(
  document: file,
  userId: '123',
  options: ApiOptions(
    onSendProgress: (sent, total) {
      print('Upload: ${(sent / total * 100).toStringAsFixed(0)}%');
    },
  ),
);
```

## Express Route Examples

### Simple GET with Path Parameter

```javascript
/**
 * Fetch a single post by ID
 * @var {string} postId - The post ID
 */
router.get('/posts/:postId', async (req, res) => {
  const post = await Post.findById(req.params.postId);
  res.json(post);
});
```

Generated Dart:
```dart
static Future<Response> getPostsById({
  required String postId,
  Map<String, dynamic>? data,
  ApiOptions? options,
}) async {
  return Api.get('/posts/${postId}', ...);
}
```

### POST with Query and Body Parameters

```javascript
/**
 * Create a new post
 * @var {string} req.query.notify - Notify followers
 * @var {string} title - Post title
 * @var {string} content - Post content
 * @var {string[]} tags - Post tags
 */
router.post('/posts', async (req, res) => {
  const post = await Post.create(req.body);
  res.json(post);
});
```

Generated Dart:
```dart
static Future<Response> posts({
  required String notify,
  required String title,
  required String content,
  required List<String> tags,
  Map<String, dynamic>? data,
  ApiOptions? options,
}) async {
  final bodyData = <String, dynamic>{
    'title': title,
    'content': content,
    'tags': tags,
  };

  final queryParams = <String, dynamic>{
    'notify': notify,
  };

  return Api.post(
    '/posts',
    data: data ?? bodyData,
    ...
  );
}
```

### File Upload

```javascript
/**
 * Upload a document
 * @var {string} userId - User ID
 * @var {file} req.body.document - Document file
 */
router.post('/users/:userId/documents', async (req, res) => {
  const doc = await Document.create({
    file: req.files.document,
    userId: req.params.userId,
  });
  res.json(doc);
});
```

Generated Dart:
```dart
static Future<Response> usersById_documents({
  required String userId,
  required File document,
  Map<String, dynamic>? data,
  ApiOptions? options,
}) async {
  final fileFormData = FormData();
  fileFormData.files.add(MapEntry(
    'document',
    await MultipartFile.fromFile(document.path,
      filename: document.path.split(Platform.pathSeparator).last),
  ));

  return Api.post(
    '/users/${userId}/documents',
    formData: fileFormData,
    ...
  );
}
```

### With Authentication & Permissions

```javascript
/**
 * Delete a user (admin only)
 * @var {string} userId - User to delete
 */
router.delete(
  '/users/:userId',
  authenticateToken,
  PermissionsMiddleware([NsUserPermissions.ADMIN_USERS]),
  async (req, res) => {
    await User.destroy({where: {id: req.params.userId}});
    res.json({success: true});
  }
);
```

Generated Dart:
```dart
static Future<Response> usersById({
  required String userId,
  Map<String, dynamic>? data,
  ApiOptions? options,
}) async {
  if (!AppUser.havePermissionFor(NsPermissions.ADMIN_USERS)) {
    throw Exception("Permission denied: NsPermissions.ADMIN_USERS");
  }

  return Api.delete('/users/${userId}', ...);
}
```

### Nested Routes with Sub-routers

```javascript
// routes/api.js
const usersRouter = require('./users');
const postsRouter = require('./posts');

router.use('/users', usersRouter);
router.use('/posts', postsRouter);

// routes/users.js
router.get('/:userId', (req, res) => { ... });
router.post('/', (req, res) => { ... });

// routes/posts.js
router.get('/:postId', (req, res) => { ... });
router.put('/:postId', (req, res) => { ... });
```

Generated routes:
- `GET /users/:userId` → `getUsersById()`
- `POST /users` → `users()`
- `GET /posts/:postId` → `getPostsById()`
- `PUT /posts/:postId` → `putPostsById()`

## CLI Commands

### Generate Dart Client

```bash
express-to-dart generate
# or
express-to-dart
```

With custom config:
```bash
express-to-dart generate --config ./config.js
```

### Initialize Configuration

```bash
express-to-dart init
```

Creates `endpoints-to-dart.config.js` in current directory.

### Help

```bash
express-to-dart --help
express-to-dart -h
```

### Version

```bash
express-to-dart --version
express-to-dart -v
```

## Programmatic API

Use the generator in your Node.js code:

```javascript
const { generate } = require('@sampathkumara/express-to-dart-endpoints-builder');

// Generate with default config
const result = generate();

if (result.success) {
  console.log(`Generated ${result.count} endpoints`);
  console.log(`Output: ${result.outputPath}`);
} else {
  console.error(result.message);
}
```

With custom config path:
```javascript
const result = generate('./custom-config.js');
```

## Advanced Features

### Custom Imports

Add custom Dart imports to generated file:

```json
{
  "imports": [
    "package:your_app/api/client.dart",
    "package:your_app/models/user.dart",
    "package:dio/dio.dart",
    "dart:convert"
  ]
}
```

### Exclude Test Files

```json
{
  "excludePatterns": [
    "**/node_modules/**",
    "**/*.test.js",
    "**/*.spec.js",
    "**/test/**"
  ]
}
```

### Nested Route Hierarchies

The generator automatically handles:
- Direct router definitions in files
- Sub-routers mounted via `router.use(path, router)`
- Array-based route definitions
- Authentication inheritance across nested routes
- Permission combination across route tree

### Method Naming Convention

Route paths are converted to camelCase method names:
- `/api/users` → `users()`
- `/api/users/:id` → `usersById()`
- `/api/users/:id/posts` → `usersById_posts()`
- `/api/v1/user-profiles/:id/details` → `v1_userProfiles_by_id_details()`

## Troubleshooting

### Routes not found

1. Check that `api.js` exists in your `routesDir`
2. Verify route definitions use `router.get/post/put/delete/patch()` syntax
3. Ensure sub-routers are mounted with `router.use(path, routerVariable)`
4. Check that variables are defined with `const router = require(...)`

### Parameters not extracted

1. Add JSDoc `@var` comments above routes
2. Use explicit prefixes: `req.body.field`, `req.query.field`, `req.params.field`
3. For path parameters, they should appear in the route path like `:paramName`
4. Ensure parameter names are valid JavaScript identifiers

### Wrong Dart types generated

1. Check type names in `@var` tags (case-sensitive for custom types)
2. Use supported types from the type mapping table
3. For array types, use one of: `string[]`, `list<string>`, `array<string>`
4. For maps, use: `map<string, int>`

### Config file not found

1. Run `express-to-dart init` first
2. Or create `endpoints-to-dart.config.js` manually
3. Or use `--config` flag to specify custom path

## Performance

The generator scans your routes directory and generates Dart code in seconds, even for large projects with hundreds of endpoints.

## Contributing

Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request

## License

MIT

## Support

For issues, questions, or suggestions:
- Open an issue on GitHub
- Check the documentation in CLAUDE.md
- Review the examples in this README

## Changelog

### v1.1.0
- **TypeScript support** - Now collects routers from `.ts` and `.tsx` files
- Smart file resolution for sub-routers (tries `.ts`, `.tsx`, then `.js`)
- API entry point detection supports `api.ts`, `api.tsx`, or `api.js`
- Default include patterns updated to include TypeScript files

### v1.0.0
- Initial release
- Full JSDoc support
- Authentication and permission handling
- File upload/download support
- Nested route discovery
- Type mapping for Dart