วันที่เจอ API docs ที่แย่ที่สุด
วันหนึ่งต้องใช้ third-party API ดู documentation แล้วอึ้ง:
GET /api/users/{id}
Description: Get user by ID
Parameters:
- id (required): User ID
Response:
- 200: Success
- 404: Not found
แค่นี้! 😱
คำถามที่มี:
idเป็น string หรือ number?- Response format เป็นยังไง?
- Authentication ต้องการไหม?
- Error format เป็นยังไง?
- Rate limiting มีไหม?
ลองเรียก API ก็ error 401 แต่ไม่บอกว่าต้องส่ง header อะไร ใช้เวลา 2 ชั่วโมงแก้ปัญหาที่ documentation ควรบอก!
ตอนนั้นแหละที่คิดว่า: “เวลาเราทำ API docs ต้องไม่ให้ใครเจอปัญหาแบบนี้!”
ช่วงมืดของ API Documentation
ในโปรเจคเก่าๆ เราใช้วิธีเขียน docs แบบนี้:
Swagger/OpenAPI แบบเขียนมือ
# swagger.yaml - เขียนมือ 500+ บรรทัด
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/api/users/{id}:
get:
summary: Get user by ID
parameters:
- name: id
in: path
required: true
schema:
type: integer
example: 123
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
Error:
type: object
properties:
message:
type: string
ปัญหาที่เจอ:
- เขียนนาน แล้วก็ sync กับ code ยาก
- Developer ไม่ค่อย update docs
- Example data ไม่จริง
- UI ดูน่าเบื่อ
README.md แบบ Manual
# API Documentation
## Authentication
Send JWT token in Authorization header: `Bearer <token>`
## Endpoints
### GET /api/users/{id}
Get user by ID
**Parameters:**
- id (integer) - User ID
**Response:**
```json
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
Errors:
- 401: Unauthorized
- 404: User not found
**ปัญหา:**
- ไม่มี interactive testing
- ไม่สามารถลอง API ได้
- ไม่มี validation
- ยาก maintain
## การค้นพบ ScalarDocs
พอได้เห็น ScalarDocs ครั้งแรก รู้สึกว่า "นี่สิคือสิ่งที่หาอยู่!"
**Features ที่ทำให้หลงรัก:**
- **Beautiful UI** - สวยกว่า Swagger UI มาก
- **Interactive** - ลอง API ได้ในหน้า docs
- **Real-time** - sync กับ OpenAPI spec อัตโนมัติ
- **Modern** - responsive, fast, accessible
- **Customizable** - theme และ branding ได้
### Setup แรกกับ Express.js
```javascript
// server.js
const express = require('express');
const swaggerJsdoc = require('swagger-jsdoc');
const { scalar } = require('@scalar/express-api-reference');
const app = express();
// Swagger JSDoc configuration
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'User API',
version: '1.0.0',
description: 'A simple Express User API',
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server',
},
],
},
apis: ['./routes/*.js'], // paths to files containing OpenAPI definitions
};
const specs = swaggerJsdoc(options);
// Scalar API Reference
app.use(
'/docs',
scalar({
spec: {
content: specs,
},
theme: 'purple', // หรือ 'blue', 'green', 'orange'
})
);
app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
console.log('API Docs: http://localhost:3000/docs');
});
JSDoc Comments ใน Route Files
// routes/users.js
const express = require('express');
const router = express.Router();
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* required:
* - name
* - email
* properties:
* id:
* type: integer
* description: The auto-generated user ID
* example: 123
* name:
* type: string
* description: The user's full name
* example: "John Doe"
* email:
* type: string
* format: email
* description: The user's email address
* example: "john@example.com"
* createdAt:
* type: string
* format: date-time
* description: Account creation timestamp
* example: "2023-01-15T10:30:00Z"
* Error:
* type: object
* properties:
* error:
* type: string
* example: "User not found"
* code:
* type: string
* example: "USER_NOT_FOUND"
*/
/**
* @swagger
* /api/users/{id}:
* get:
* summary: Get user by ID
* tags: [Users]
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: Numeric ID of the user to retrieve
* example: 123
* responses:
* 200:
* description: User found successfully
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/User'
* examples:
* admin_user:
* summary: Admin user example
* value:
* id: 1
* name: "Admin User"
* email: "admin@example.com"
* role: "admin"
* createdAt: "2023-01-01T00:00:00Z"
* regular_user:
* summary: Regular user example
* value:
* id: 123
* name: "John Doe"
* email: "john@example.com"
* role: "user"
* createdAt: "2023-06-15T14:30:00Z"
* 401:
* description: Authentication required
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* error: "Authentication token required"
* code: "AUTH_REQUIRED"
* 404:
* description: User not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* example:
* error: "User not found"
* code: "USER_NOT_FOUND"
* security:
* - bearerAuth: []
*/
router.get('/users/:id', async (req, res) => {
try {
const user = await getUserById(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
code: 'USER_NOT_FOUND'
});
}
res.json(user);
} catch (error) {
res.status(500).json({
error: 'Internal server error',
code: 'INTERNAL_ERROR'
});
}
});
module.exports = router;
Advanced Features ที่เรียนรู้
1. Authentication Examples
// swagger config with security schemes
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'User API',
version: '1.0.0',
},
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Enter your JWT token'
},
apiKey: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key',
description: 'Enter your API key'
}
}
},
security: [
{
bearerAuth: []
}
]
},
apis: ['./routes/*.js'],
};
/**
* @swagger
* /api/auth/login:
* post:
* summary: User login
* tags: [Authentication]
* security: [] # No auth required for login
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - email
* - password
* properties:
* email:
* type: string
* format: email
* example: "user@example.com"
* password:
* type: string
* format: password
* example: "securePassword123"
* examples:
* valid_credentials:
* summary: Valid login credentials
* value:
* email: "john@example.com"
* password: "mypassword123"
* responses:
* 200:
* description: Login successful
* content:
* application/json:
* schema:
* type: object
* properties:
* token:
* type: string
* example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* user:
* $ref: '#/components/schemas/User'
* 401:
* description: Invalid credentials
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
2. Complex Request/Response Examples
/**
* @swagger
* /api/users:
* post:
* summary: Create a new user
* tags: [Users]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - email
* - password
* properties:
* name:
* type: string
* minLength: 2
* maxLength: 50
* example: "John Doe"
* email:
* type: string
* format: email
* example: "john@example.com"
* password:
* type: string
* minLength: 8
* format: password
* description: "Password must be at least 8 characters long"
* example: "securePassword123"
* profile:
* type: object
* properties:
* bio:
* type: string
* maxLength: 500
* example: "Software developer passionate about API design"
* website:
* type: string
* format: uri
* example: "https://johndoe.dev"
* social:
* type: object
* properties:
* twitter:
* type: string
* example: "@johndoe"
* github:
* type: string
* example: "johndoe"
* examples:
* minimal_user:
* summary: Minimal user creation
* value:
* name: "Jane Smith"
* email: "jane@example.com"
* password: "password123"
* full_profile:
* summary: User with complete profile
* value:
* name: "John Developer"
* email: "john@example.com"
* password: "securePass456"
* profile:
* bio: "Full-stack developer with 5 years experience"
* website: "https://johndeveloper.com"
* social:
* twitter: "@johndev"
* github: "johndev"
* responses:
* 201:
* description: User created successfully
* content:
* application/json:
* schema:
* allOf:
* - $ref: '#/components/schemas/User'
* - type: object
* properties:
* profile:
* type: object
* properties:
* bio:
* type: string
* website:
* type: string
* social:
* type: object
* 400:
* description: Validation error
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* example: "Validation failed"
* details:
* type: array
* items:
* type: object
* properties:
* field:
* type: string
* message:
* type: string
* example:
* error: "Validation failed"
* details:
* - field: "email"
* message: "Email is already in use"
* - field: "password"
* message: "Password must be at least 8 characters"
*/
3. File Upload Documentation
/**
* @swagger
* /api/users/{id}/avatar:
* post:
* summary: Upload user avatar
* tags: [Users]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: integer
* example: 123
* requestBody:
* required: true
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* avatar:
* type: string
* format: binary
* description: "Avatar image file (JPG, PNG, max 5MB)"
* crop:
* type: object
* properties:
* x:
* type: integer
* example: 0
* y:
* type: integer
* example: 0
* width:
* type: integer
* example: 200
* height:
* type: integer
* example: 200
* encoding:
* avatar:
* contentType: image/png, image/jpeg
* responses:
* 200:
* description: Avatar uploaded successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* avatarUrl:
* type: string
* format: uri
* example: "https://example.com/avatars/123.jpg"
* thumbnails:
* type: object
* properties:
* small:
* type: string
* example: "https://example.com/avatars/123_small.jpg"
* medium:
* type: string
* example: "https://example.com/avatars/123_medium.jpg"
*/
Customization และ Branding
1. Custom Theme
// Custom Scalar configuration
app.use(
'/docs',
scalar({
spec: {
content: specs,
},
configuration: {
theme: 'purple',
layout: 'modern', // หรือ 'classic'
customCss: `
.scalar-app {
--scalar-color-1: #1a1a2e;
--scalar-color-2: #16213e;
--scalar-color-3: #0f3460;
--scalar-color-accent: #e94560;
--scalar-font-size: 14px;
}
.scalar-app .logo {
content: url('https://yoursite.com/logo.svg');
}
.scalar-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
`,
metaData: {
title: 'My Awesome API Documentation',
description: 'The best API documentation you\'ve ever seen',
ogDescription: 'Check out our amazing API',
},
searchHotKey: 'k',
hideDownloadButton: false,
hideTestRequestButton: false,
},
})
);
2. Advanced Layout Options
// Multiple documentation versions
app.use('/docs/v1', scalar({
spec: { content: specsV1 },
configuration: {
theme: 'blue',
metaData: { title: 'API v1.0 Documentation' }
}
}));
app.use('/docs/v2', scalar({
spec: { content: specsV2 },
configuration: {
theme: 'purple',
metaData: { title: 'API v2.0 Documentation' }
}
}));
// Internal vs External docs
app.use('/docs/internal', scalar({
spec: { content: internalSpecs },
configuration: {
authentication: {
preferredSecurityScheme: 'bearerAuth',
apiKey: {
token: process.env.INTERNAL_API_KEY
}
}
}
}));
Integration กับ CI/CD
1. Automated Docs Generation
# .github/workflows/docs.yml
name: Generate API Documentation
on:
push:
branches: [main]
paths: ['routes/**', 'swagger.yaml']
jobs:
generate-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Generate OpenAPI spec
run: npm run generate-swagger
- name: Validate OpenAPI spec
run: npx swagger-codegen-cli validate -i ./swagger.json
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
2. API Diff Detection
// scripts/check-api-changes.js
const swaggerDiff = require('swagger-diff');
const fs = require('fs');
const oldSpec = JSON.parse(fs.readFileSync('./swagger-old.json'));
const newSpec = JSON.parse(fs.readFileSync('./swagger-new.json'));
swaggerDiff(oldSpec, newSpec, {
check: true,
markdown: true
}).then(diff => {
if (diff.breaks > 0) {
console.error('🚨 Breaking changes detected!');
console.log(diff.markdown);
process.exit(1);
} else if (diff.changes > 0) {
console.log('✅ Non-breaking changes detected');
console.log(diff.markdown);
} else {
console.log('✅ No API changes');
}
}).catch(err => {
console.error('Error comparing API specs:', err);
process.exit(1);
});
Testing Documentation
1. Documentation Tests
// tests/docs.test.js
const request = require('supertest');
const app = require('../server');
const swaggerSpec = require('../swagger.json');
describe('API Documentation', () => {
test('should serve documentation at /docs', async () => {
const response = await request(app)
.get('/docs')
.expect(200);
expect(response.text).toContain('Scalar API Reference');
});
test('should have valid OpenAPI spec', () => {
expect(swaggerSpec.openapi).toBe('3.0.0');
expect(swaggerSpec.info).toBeDefined();
expect(swaggerSpec.paths).toBeDefined();
});
test('all endpoints should be documented', async () => {
const routes = getExpressRoutes(app);
const documentedPaths = Object.keys(swaggerSpec.paths);
routes.forEach(route => {
const pathExists = documentedPaths.some(path =>
path.replace(/{([^}]+)}/g, ':$1') === route.path
);
expect(pathExists).toBe(true);
});
});
});
// Contract testing
describe('API Contract Tests', () => {
test('GET /api/users/{id} should match documentation', async () => {
const response = await request(app)
.get('/api/users/1')
.set('Authorization', 'Bearer test-token')
.expect(200);
// Validate response against schema
const userSchema = swaggerSpec.components.schemas.User;
expect(response.body).toMatchSchema(userSchema);
});
});
2. Example Validation
// scripts/validate-examples.js
const Ajv = require('ajv');
const addFormats = require('ajv-formats');
const ajv = new Ajv();
addFormats(ajv);
const validateExamples = (spec) => {
const errors = [];
Object.entries(spec.paths).forEach(([path, methods]) => {
Object.entries(methods).forEach(([method, operation]) => {
// Validate request examples
if (operation.requestBody) {
const content = operation.requestBody.content;
Object.entries(content).forEach(([mediaType, mediaTypeObj]) => {
if (mediaTypeObj.examples) {
Object.entries(mediaTypeObj.examples).forEach(([exampleName, example]) => {
const validate = ajv.compile(mediaTypeObj.schema);
if (!validate(example.value)) {
errors.push({
path,
method,
type: 'request',
example: exampleName,
errors: validate.errors
});
}
});
}
});
}
// Validate response examples
if (operation.responses) {
Object.entries(operation.responses).forEach(([status, response]) => {
if (response.content) {
Object.entries(response.content).forEach(([mediaType, mediaTypeObj]) => {
if (mediaTypeObj.example && mediaTypeObj.schema) {
const validate = ajv.compile(mediaTypeObj.schema);
if (!validate(mediaTypeObj.example)) {
errors.push({
path,
method,
type: 'response',
status,
errors: validate.errors
});
}
}
});
}
});
}
});
});
return errors;
};
Real-World Tips ที่เรียนรู้
1. Documentation as Code
// utils/doc-helpers.js - Helper functions สำหรับ documentation
const createApiResponse = (description, schema, examples = {}) => ({
description,
content: {
'application/json': {
schema,
examples: Object.keys(examples).length > 0 ? examples : undefined
}
}
});
const createErrorResponse = (status, message, code) => ({
[status]: createApiResponse(
message,
{ $ref: '#/components/schemas/Error' },
{
example: {
summary: `${status} error example`,
value: { error: message, code }
}
}
)
});
// Usage in route files
/**
* @swagger
* /api/users/{id}:
* get:
* summary: Get user by ID
* responses:
* 200:
* <<: *userSuccessResponse
* 401:
* <<: *authErrorResponse
* 404:
* <<: *notFoundResponse
*/
2. Versioning Strategy
// Version-specific documentation
const generateVersionedSpec = (version) => {
const baseSpec = {
openapi: '3.0.0',
info: {
title: 'My API',
version: version,
description: `API Documentation for version ${version}`
}
};
// Version-specific changes
switch (version) {
case 'v1':
return {
...baseSpec,
paths: v1Paths,
components: v1Components
};
case 'v2':
return {
...baseSpec,
paths: { ...v1Paths, ...v2Paths },
components: { ...v1Components, ...v2Components }
};
}
};
// Route setup
app.use('/docs/v1', scalar({
spec: { content: generateVersionedSpec('v1') }
}));
app.use('/docs/v2', scalar({
spec: { content: generateVersionedSpec('v2') }
}));
3. Interactive Examples
/**
* @swagger
* /api/search:
* get:
* summary: Search users
* parameters:
* - name: q
* in: query
* required: true
* schema:
* type: string
* minLength: 2
* example: "john"
* description: Search query (minimum 2 characters)
* - name: limit
* in: query
* schema:
* type: integer
* minimum: 1
* maximum: 100
* default: 10
* example: 20
* - name: offset
* in: query
* schema:
* type: integer
* minimum: 0
* default: 0
* example: 0
* - name: sort
* in: query
* schema:
* type: string
* enum: [name, email, created_at]
* default: name
* example: "name"
* responses:
* 200:
* description: Search results
* content:
* application/json:
* schema:
* type: object
* properties:
* users:
* type: array
* items:
* $ref: '#/components/schemas/User'
* pagination:
* type: object
* properties:
* total:
* type: integer
* example: 150
* limit:
* type: integer
* example: 20
* offset:
* type: integer
* example: 0
* hasMore:
* type: boolean
* example: true
* examples:
* search_results:
* summary: Typical search results
* value:
* users:
* - id: 1
* name: "John Doe"
* email: "john@example.com"
* - id: 25
* name: "John Smith"
* email: "johnsmith@example.com"
* pagination:
* total: 2
* limit: 20
* offset: 0
* hasMore: false
* empty_results:
* summary: No results found
* value:
* users: []
* pagination:
* total: 0
* limit: 20
* offset: 0
* hasMore: false
*/
Performance และ SEO
1. Static Site Generation
// scripts/generate-static-docs.js
const fs = require('fs');
const path = require('path');
const { renderToString } = require('@scalar/api-reference');
const generateStaticDocs = async () => {
const spec = require('../swagger.json');
const html = renderToString({
spec: { content: spec },
configuration: {
theme: 'purple',
layout: 'modern',
metaData: {
title: 'My API Documentation',
description: 'Complete API documentation with examples'
}
}
});
const fullHTML = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>API Documentation</title>
<meta name="description" content="Complete API documentation with examples">
<meta property="og:title" content="My API Documentation">
<meta property="og:description" content="Interactive API documentation">
<meta property="og:type" content="website">
<link rel="canonical" href="https://api.mysite.com/docs">
</head>
<body>
${html}
</body>
</html>`;
fs.writeFileSync('./dist/docs.html', fullHTML);
};
2. CDN และ Caching
// Production optimizations
app.use('/docs',
// Cache static assets
express.static('public', {
maxAge: '1y',
etag: false
}),
scalar({
spec: { content: specs },
configuration: {
// CDN สำหรับ assets
cdn: 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest',
// Preload critical resources
preloadFonts: true,
// Optimize bundle
minify: process.env.NODE_ENV === 'production'
}
})
);
สรุป: Documentation ที่ทำให้คนอยากใช้ API
ก่อนใช้ ScalarDocs:
- Documentation น่าเบื่อ และใช้ยาก
- Developer บ่น API ไม่มี docs
- Support tickets เยอะเรื่อง API usage
- Integration ช้าเพราะไม่เข้าใจ API
หลังใช้ ScalarDocs:
- Documentation สวยและใช้งานง่าย
- Developer สามารถเริ่มใช้ API ได้เลย
- Support tickets ลดลง 60%
- Integration เร็วขึ้นมาก
ข้อดีที่ได้จริง:
- Beautiful UI ที่ทำให้อยากอ่าน docs
- Interactive testing ลอง API ได้ในหน้า docs
- Real-time sync ระหว่าง code และ docs
- Better DX developer experience ดีขึ้นเยอะ
- SEO friendly มี meta tags และ structured data
Best Practices ที่เรียนรู้:
- Write docs first - design API ผ่าน documentation
- Use examples extensively - real data ดีกว่า placeholder
- Test your docs - validate examples และ schemas
- Keep it updated - automate ให้มาก
- Think like users - เขียน docs ให้คนใช้เข้าใจง่าย
ScalarDocs มันเหมือน iPhone ของ API documentation
สวย ใช้งานง่าย และทำให้คนอยากใช้ 📱✨
ตอนนี้ API docs ไม่ใช่สิ่งที่น่าเบื่ออีกต่อไป แต่กลายเป็น selling point ของ API เลย! 🎯