Modern Swagger/OpenAPI integration for AdonisJS v6 with Scalar UI.
- 🚀 Modern UI: Uses Scalar instead of traditional Swagger UI
- 🎯 Decorator-driven: Documentation only generated for routes with explicit decorators
- 🔧 Schema Support: Native TypeBox, Zod, and VineJS schema support alongside raw JSON Schema
- 🧩 Components Feature: Automatic OpenAPI component schema generation for
openapi-typescriptintegration - 🎨 Customizable: Flexible configuration and theming options
- 🛠️ CLI Integration: Seamless integration with AdonisJS Ace commands
- 📦 TypeScript: Full TypeScript support with proper type definitions
- ⚡ Dynamic Loading: Leverages AdonisJS v6's lazy loading for optimal performance
- 🏗️ Universal Compatibility: Works with all AdonisJS starter kits (API, Slim, Web, Inertia)
- 🔧 Zero Dependencies: No need to install Edge.js separately - included in the package
# Core package (includes everything you need)
node ace add adonis-open-swagger
# Optional: Install schema libraries based on your preference
npm install @sinclair/typebox # For TypeBox schemas
npm install zod # For Zod schemas
npm install @vinejs/vine # For VineJS schemas (recommended for AdonisJS)
# Optional: For TypeScript type generation from OpenAPI components
npm install -D openapi-typescript # Generates TypeScript types from OpenAPI specs- No additional dependencies required: The package includes Edge.js for template rendering
- Works with all AdonisJS starter kits: API, Slim, Web, and Inertia
- Schema libraries: Optional dependencies based on your validation needs
Note: TypeBox, Zod v4+, and VineJS are optional dependencies. The package will work with raw JSON Schema even if these libraries are not installed. Schema conversion will gracefully fallback if the libraries are missing.
Configure the package in your AdonisJS application:
node ace configure adonis-open-swaggerThis will:
- Register the service provider
- Create a
config/swagger.tsconfiguration file - Add example routes in
start/swagger_routes.ts - Register Ace commands
After configuration, your API documentation will be available at /docs (configurable).
Important: This package uses a decorator-driven approach. Only routes with explicit decorators will appear in the documentation. Routes without decorators will not be documented.
The package creates a config/swagger.ts file:
import { defineConfig } from 'adonis-open-swagger'
export default defineConfig({
enabled: true,
path: '/docs',
validator: 'vinejs', // or 'zod', 'typebox'
info: {
title: 'My API',
version: '1.0.0',
description: 'API documentation for my application',
},
scalar: {
theme: 'auto', // 'auto' | 'default' | 'moon' | 'purple' | 'solarized' | 'bluePlanet' | 'saturn' | 'kepler' | 'mars' | 'deepSpace' | 'laserwave' | 'elysiajs' | 'none'
layout: 'modern', // 'modern' or 'classic'
},
routes: {
include: ['/api/*'],
exclude: ['/docs*', '/health*'],
},
components: {
include: ['app/schemas'], // Optional: for openapi-typescript integration
},
})The Components feature automatically generates OpenAPI component schemas from your validation schema files, enabling proper TypeScript type generation with openapi-typescript instead of schemas: never.
Add a components section to your config/swagger.ts:
export default defineConfig({
// ... other configuration
components: {
/**
* Array of file paths or directory paths to include schemas
* Supports multiple files, directories, and patterns
*/
include: [
'app/schemas/index.ts', // Single file
'app/schemas/product.schema.ts', // Another file
'app/models', // Entire directory (recursive)
'app/validators/*.ts', // Pattern matching
],
/**
* Optional: Exclude patterns for schema files
* Array of glob patterns to exclude specific files
*/
exclude: ['**/*.test.ts', '**/*.spec.ts'],
},
})Create schema files using VineJS, Zod, or TypeBox:
// app/schemas/user.schema.ts
import vine from '@vinejs/vine'
export const userSchema = vine.object({
id: vine.string(),
name: vine.string(),
email: vine.string().email(),
age: vine.number().optional(),
createdAt: vine.string(),
updatedAt: vine.string(),
})
export const createUserSchema = vine.object({
name: vine.string().minLength(2),
email: vine.string().email(),
age: vine.number().optional(),
})// app/schemas/product.schema.ts
import { z } from 'zod'
export const productSchema = z.object({
id: z.string(),
name: z.string(),
price: z.number().positive(),
inStock: z.boolean(),
tags: z.array(z.string()),
})// app/models/order.schema.ts
import { Type } from '@sinclair/typebox'
export const orderSchema = Type.Object({
id: Type.String(),
userId: Type.String(),
total: Type.Number({ minimum: 0 }),
status: Type.Union([Type.Literal('pending'), Type.Literal('confirmed'), Type.Literal('shipped')]),
})After configuring components, generate TypeScript types:
# Start your AdonisJS server
npm run dev
# Generate TypeScript types from OpenAPI spec
npx openapi-typescript http://localhost:3333/docs/json -o ./types/api-schema.tsNow you can use fully-typed API components in your frontend:
// Frontend usage
import { components } from './types/api-schema'
// Extract component types - fully typed!
type User = components['schemas']['userSchema']
type CreateUser = components['schemas']['createUserSchema']
type Product = components['schemas']['productSchema']
type Order = components['schemas']['orderSchema']
// Use in your application with full type safety
export class ApiClient {
async createUser(data: CreateUser): Promise<User> {
// TypeScript ensures correct data structure
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return response.json()
}
async getUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`)
return response.json()
}
async createProduct(data: Omit<Product, 'id'>): Promise<Product> {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
return response.json()
}
}✅ Type Safety: Full TypeScript type safety for API components ✅ IntelliSense: Complete IDE support with autocomplete ✅ Consistency: Ensures frontend and backend stay in sync ✅ Zero Runtime Cost: Pure TypeScript types with no runtime overhead ✅ Multi-Library Support: Works with VineJS, Zod, and TypeBox ✅ Flexible Organization: Support for multiple files and directories
Enhance your controllers with documentation decorators using TypeBox, Zod, VineJS, or raw JSON Schema:
import { HttpContext } from '@adonisjs/core/http'
import { Type } from '@sinclair/typebox'
import { z } from 'zod'
import vine from '@vinejs/vine'
import { SwaggerInfo, SwaggerResponse, SwaggerRequestBody, SwaggerParam } from 'adonis-open-swagger'
// TypeBox schemas
const UserSchema = Type.Object({
id: Type.Integer(),
name: Type.String(),
email: Type.String({ format: 'email' }),
createdAt: Type.String({ format: 'date-time' }),
})
const UserListResponseSchema = Type.Object({
data: Type.Array(UserSchema),
meta: Type.Object({
total: Type.Integer(),
page: Type.Integer(),
limit: Type.Integer(),
}),
})
// Zod schemas
const CreateUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z.string().min(8),
})
// VineJS schemas (recommended for AdonisJS v6)
const UpdateUserSchema = vine.object({
name: vine.string().minLength(2).maxLength(100).optional(),
email: vine.string().email().optional(),
})
export default class UsersController {
@SwaggerInfo({
tags: ['Users'],
summary: 'Get all users',
description: 'Retrieve a paginated list of users',
})
@SwaggerParam(
{
name: 'page',
location: 'query',
description: 'Page number',
},
Type.Integer({ minimum: 1 })
)
@SwaggerParam(
{
name: 'limit',
location: 'query',
description: 'Items per page',
},
Type.Integer({ minimum: 1, maximum: 100 })
)
@SwaggerResponse(200, 'Users retrieved successfully', UserListResponseSchema)
async index({ request, response }: HttpContext) {
// Your implementation
}
@SwaggerInfo({
tags: ['Users'],
summary: 'Create new user',
description: 'Create a new user with validation',
})
@SwaggerRequestBody('User data', CreateUserSchema) // Using Zod schema
@SwaggerResponse(201, 'User created successfully', UserSchema) // Using TypeBox schema
@SwaggerResponse(400, 'Validation error')
async store({ request, response }: HttpContext) {
// Your implementation
}
@SwaggerInfo({
tags: ['Users'],
summary: 'Update user',
description: 'Update an existing user with validation',
})
@SwaggerRequestBody('User update data', UpdateUserSchema) // Using VineJS schema
@SwaggerResponse(200, 'User updated successfully', UserSchema)
async update({ request, response }: HttpContext) {
// Your implementation
}
}All decorators support TypeBox schemas, Zod schemas, VineJS schemas, and raw JSON Schema objects:
@SwaggerInfo(options)- Primary decorator combining tags, summary, and description ⭐@SwaggerParam(options, schema, required?)- Enhanced parameter decorator for query/path ⭐@SwaggerHeader(options, schema, required?)- Header parameter decorator ⭐@SwaggerResponse(status, description, schema?)- Define response (supports TypeBox/Zod/VineJS/JSON Schema)@SwaggerRequestBody(description, schema, options?)- Define request body with optionalcontentTypefor file uploads (supports TypeBox/Zod/VineJS/JSON Schema)@SwaggerDeprecated(deprecated?)- Mark as deprecated@SwaggerSecurity(security)- Define security requirements@Swagger(options)- Combined decorator for common options
You can use any of these schema formats (install the corresponding packages as needed):
// TypeBox schema (requires: npm install @sinclair/typebox)
@SwaggerResponse(200, 'Success', Type.Object({
id: Type.Integer(),
name: Type.String()
}))
// Zod schema (requires: npm install zod)
@SwaggerRequestBody('User data', z.object({
name: z.string(),
email: z.string().email()
}))
// VineJS schema (requires: npm install @vinejs/vine) - Recommended for AdonisJS v6
@SwaggerRequestBody('User data', vine.object({
name: vine.string().minLength(2),
email: vine.string().email()
}))
// Raw JSON Schema (no additional dependencies)
@SwaggerResponse(200, 'Success', {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' }
}
})The @SwaggerRequestBody decorator supports file uploads via the contentType option and file helper functions:
import {
SwaggerRequestBody,
SwaggerInfo,
openapiFile,
vineFile, // Alias for VineJS users
typeboxFile, // Alias for TypeBox users
zodFile, // Alias for Zod users
} from 'adonis-open-swagger'
import vine from '@vinejs/vine'
export default class CampaignsController {
@SwaggerInfo({
tags: ['Campaigns'],
summary: 'Create campaign with file upload',
description: 'Upload a CSV file with customer data',
})
@SwaggerRequestBody(
'Campaign data with file',
vine.object({
name: vine.string(),
file: vineFile({ description: 'CSV file with customers' }),
}),
{ contentType: 'multipart/form-data' }
)
async store({ request }: HttpContext) {
// Handle file upload
}
}| Function | Description |
|---|---|
openapiFile(options?) |
Generic file schema (works with any validator) |
vineFile(options?) |
Alias for VineJS users |
typeboxFile(options?) |
Alias for TypeBox users |
zodFile(options?) |
Alias for Zod users |
interface FileOptions {
description?: string // Description of the file field
multiple?: boolean // Accept multiple files (default: false)
minItems?: number // Min files (only when multiple: true)
maxItems?: number // Max files (only when multiple: true)
}// Single file upload
openapiFile({ description: 'Profile picture' })
// Generates: { type: "string", format: "binary" }
// Multiple files upload
openapiFile({
description: 'Gallery images',
multiple: true,
minItems: 1,
maxItems: 10,
})
// Generates: { type: "array", items: { type: "string", format: "binary" }, minItems: 1, maxItems: 10 }The @SwaggerRequestBody decorator supports three content types:
| Content Type | Use Case |
|---|---|
application/json |
Default for JSON payloads |
multipart/form-data |
File uploads and mixed data |
application/x-www-form-urlencoded |
Form submissions |
// JSON (default)
@SwaggerRequestBody('User data', userSchema)
// File upload
@SwaggerRequestBody('File upload', fileSchema, { contentType: 'multipart/form-data' })
// Form data
@SwaggerRequestBody('Form data', formSchema, { contentType: 'application/x-www-form-urlencoded' })
// Optional request body
@SwaggerRequestBody('Optional data', schema, { required: false })# Generate JSON and YAML files
node ace swagger:generate
# Generate only JSON
node ace swagger:generate --format=json
# Generate to custom directory
node ace swagger:generate --output=./docs# Validate the generated OpenAPI spec
node ace swagger:validate
# Show detailed validation output
node ace swagger:validate --verboseinfo: {
title: 'My API',
version: '1.0.0',
description: 'API description',
contact: {
name: 'API Support',
email: 'support@example.com',
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT',
},
}scalar: {
theme: 'auto', // 'auto' | 'default' | 'moon' | 'purple' | 'solarized' | 'bluePlanet' | 'saturn' | 'kepler' | 'mars' | 'deepSpace' | 'laserwave' | 'elysiajs' | 'none'
layout: 'modern', // 'modern' or 'classic'
showSidebar: true,
customCss: `
/* Your custom styles */
.scalar-app { font-family: 'Custom Font'; }
`,
configuration: {
// Additional Scalar configuration options
},
}'auto'- Automatically detects system preference (light/dark mode)'default'- Scalar's default light theme'moon'- Dark theme with blue accents'purple'- Purple-themed interface'solarized'- Based on the popular Solarized color scheme'bluePlanet'- Blue-themed space design'saturn'- Saturn-inspired theme'kepler'- Space exploration theme'mars'- Mars-inspired red theme'deepSpace'- Dark space theme'laserwave'- Retro synthwave theme'elysiajs'- ElysiaJS-inspired theme'none'- No theme (for custom styling)
Open Swagger supports both traditional string-based route handlers and the modern array-based import format recommended in AdonisJS v6:
// start/routes.ts
import router from '@adonisjs/core/services/router'
// Define import functions
const AuthController = () => import('#controllers/auth_controller')
const UsersController = () => import('#controllers/users_controller')
// Use array format: [ImportFunction, 'methodName']
router
.group(() => {
router.post('login', [AuthController, 'login']).as('login')
router.post('logout', [AuthController, 'logout']).as('logout')
router.post('register', [AuthController, 'register']).as('register')
})
.as('auth')
.prefix('api/auth')
router
.group(() => {
router.get('users', [UsersController, 'index'])
router.get('users/:id', [UsersController, 'show'])
router.post('users', [UsersController, 'store'])
router.put('users/:id', [UsersController, 'update'])
router.delete('users/:id', [UsersController, 'destroy'])
})
.prefix('api/v1')// start/routes.ts
import router from '@adonisjs/core/services/router'
// Use string format: '#controllers/controller_name.methodName'
router
.group(() => {
router.post('login', '#controllers/auth_controller.login')
router.post('logout', '#controllers/auth_controller.logout')
router.post('register', '#controllers/auth_controller.register')
})
.prefix('api/auth')You can use both formats in the same application:
// start/routes.ts
import router from '@adonisjs/core/services/router'
const UsersController = () => import('#controllers/users_controller')
router
.group(() => {
// Array handler (recommended)
router.get('profile', [UsersController, 'profile'])
// String handler (alternative format)
router.get('settings', '#controllers/users_controller.settings')
})
.prefix('api/user')Benefits of Array Handler Format:
- ✅ Better TypeScript support and IntelliSense
- ✅ Lazy loading of controllers (better performance)
- ✅ Explicit import dependencies
- ✅ Recommended by AdonisJS v6 documentation
routes: {
include: ['/api/*', '/v1/*'],
exclude: ['/docs*', '/health*', '/admin/*'],
}Note: Only routes with explicit decorators will be documented, regardless of include/exclude patterns. The filtering options help optimize which routes are scanned for decorators.
Open Swagger supports all OpenAPI 3.0 security schemes including Cookie Authentication (useful for session-based auth like Better-Auth):
// config/swagger.ts
export default defineConfig({
// ... other config
securitySchemes: {
// Cookie Authentication (for session-based auth)
cookieAuth: {
type: 'apiKey',
in: 'cookie',
name: 'session', // or 'JSESSIONID', 'auth_token', etc.
description: 'Session cookie for authentication',
},
// Bearer Token (JWT) Authentication
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Enter your JWT token',
},
// API Key in Header
apiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key',
description: 'API key passed in header',
},
// Basic Authentication
basicAuth: {
type: 'http',
scheme: 'basic',
description: 'Basic HTTP authentication',
},
// OAuth2 Authentication
oauth2Auth: {
type: 'oauth2',
description: 'OAuth2 authentication',
flows: {
authorizationCode: {
authorizationUrl: 'https://example.com/oauth/authorize',
tokenUrl: 'https://example.com/oauth/token',
scopes: {
'read:users': 'Read user information',
'write:users': 'Modify user information',
},
},
},
},
},
// Global security (applied to all endpoints)
security: [
{ cookieAuth: [] },
// { bearerAuth: [] },
],
})Apply security requirements to specific endpoints:
import { SwaggerInfo, SwaggerSecurity, SwaggerResponse } from 'adonis-open-swagger'
export default class UsersController {
// Use cookie authentication for this endpoint
@SwaggerInfo({
tags: ['Users'],
summary: 'Get current user profile',
})
@SwaggerSecurity([{ cookieAuth: [] }])
@SwaggerResponse(200, 'User profile retrieved')
async profile({ auth, response }: HttpContext) {
return response.ok(auth.user)
}
// Public endpoint (no authentication required)
@SwaggerInfo({
tags: ['Users'],
summary: 'Get public user info',
})
@SwaggerSecurity([]) // Empty array = no security required
@SwaggerResponse(200, 'Public user info')
async publicInfo({ response }: HttpContext) {
return response.ok({ message: 'Public info' })
}
// OAuth2 with specific scopes
@SwaggerInfo({
tags: ['Admin'],
summary: 'Delete user',
})
@SwaggerSecurity([{ oauth2Auth: ['write:users'] }])
@SwaggerResponse(204, 'User deleted')
async destroy({ params, response }: HttpContext) {
// Delete user logic
return response.noContent()
}
}| Type | Description | Example Use Case |
|---|---|---|
apiKey (cookie) |
API key sent in a cookie | Session-based auth, Better-Auth |
apiKey (header) |
API key sent in a header | API key authentication |
apiKey (query) |
API key sent as query param | Legacy API authentication |
http (bearer) |
Bearer token in Authorization header | JWT authentication |
http (basic) |
Basic HTTP authentication | Simple username/password |
oauth2 |
OAuth 2.0 flows | Third-party integrations |
openIdConnect |
OpenID Connect Discovery | SSO integrations |
The components configuration supports AdonisJS import aliases defined in your package.json imports field:
components: {
/**
* Array of file paths or directory paths to include schemas
* Supports import aliases, patterns, files, and directories
*/
include: [
'#schemas/*', // Import alias wildcard - expands to all files in app/schemas/
'#models/*', // Import alias wildcard - expands to all files in app/models/
'#schemas/index', // Specific import alias
'app/schemas/index.ts', // Single file (regular path)
'app/schemas/*.ts', // Pattern matching (regular path)
'app/models', // Directory (recursive)
'app/validators/schemas.ts', // Specific file
],
/**
* Optional: Exclude patterns for schema files
* Array of glob patterns to exclude specific files
*/
exclude: [
'**/*.test.ts',
'**/*.spec.ts',
'**/internal/*.ts',
],
}If your package.json contains import aliases:
{
"imports": {
"#schemas/*": "./app/schemas/*.js",
"#models/*": "./app/models/*.js",
"#schemas/index": "./app/schemas/index.js"
}
}You can use them directly in your components configuration:
#schemas/*- Automatically expands to all.tsand.jsfiles inapp/schemas/#schemas/index- Resolves to the specificapp/schemas/index.tsfile- Regular paths - Still supported alongside import aliases
The components feature automatically:
- Resolves AdonisJS import aliases from
package.jsonimports field - Expands wildcard patterns to find all matching files
- Scans specified files and directories for schema exports
- Detects VineJS, Zod, and TypeBox schemas by naming conventions and structure
- Converts schemas to OpenAPI component schemas
- Enables proper TypeScript type generation with
openapi-typescript
customSpec: {
components: {
schemas: {
User: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string' },
email: { type: 'string', format: 'email' },
},
},
},
},
}See the examples/ directory for complete examples:
controller_example.ts- Controller with TypeBox and Zod decoratorsconfig_example.ts- Advanced configuration
The package uses Edge.js internally for template rendering. If you encounter any template-related issues:
- No action required: Edge.js is included as a dependency
- Works everywhere: Compatible with all AdonisJS starter kits
- Self-contained: No additional Edge installation needed
If your routes don't appear in the documentation:
- Check decorators: Only routes with explicit decorators are documented
- Verify route patterns: Check your
include/excludepatterns in config - Controller resolution: Ensure controllers can be imported correctly
If schemas aren't converting properly:
- Install schema libraries: Ensure TypeBox, Zod, or VineJS are installed if you're using them
- Check schema format: Verify your schemas match the expected format
- Fallback behavior: The package will fallback to raw JSON Schema if conversion fails
If the components feature isn't working as expected:
- Check file paths: Ensure the
includepaths in your components configuration point to existing files/directories - Verify exports: Make sure your schema files export schemas with names ending in "Schema" (e.g.,
userSchema,productSchema) - Schema detection: The feature automatically detects VineJS, Zod, and TypeBox schemas - ensure you're using supported patterns
- Check console warnings: Look for warning messages about missing files or conversion errors
- Validate generated spec: Use
node ace swagger:validateto check if component schemas are properly included - Test with openapi-typescript: Run
npx openapi-typescript http://localhost:3333/docs/json -o ./types/api-schema.tsto verify TypeScript generation
Contributions are welcome! Please read our contributing guidelines and submit pull requests.
MIT License - see LICENSE file for details.