04 — API Design & Conventions
Dokumen ini menjelaskan aturan dan konvensi yang berlaku untuk semua endpoint API ELJoy.
Base URL
| Environment | Base URL |
|---|---|
| Development | http://localhost:3001/api/v1 |
| Staging | https://api-staging.eljoy.id/api/v1 |
| Production | https://api.eljoy.id/api/v1 |
Versioning
API menggunakan URL-based versioning: /api/v1/...
- Versi baru (
v2) hanya dibuat kalau ada breaking change - Non-breaking changes (tambah field, tambah endpoint) langsung di
v1 - Versi lama di-maintain minimal 6 bulan setelah versi baru rilis
Request Format
Headers
Content-Type: application/json
Authorization: Bearer <session-token>
Accept-Language: id # 'id' atau 'en'
X-Request-Id: <unique-request-id> # Opsional, untuk tracing
Query Parameters
?page=1&limit=20 # Pagination
?sort=created_at&order=desc # Sorting
?search=meeting # Full-text search
?filter[status]=active # Filtering
Response Format
Sukses (Single Resource)
{
"success": true,
"data": {
"id": "abc123",
"name": "John Doe",
"email": "john@example.com"
}
}
Sukses (List / Collection)
{
"success": true,
"data": [
{ "id": "abc123", "name": "Meeting Discussion" },
{ "id": "def456", "name": "Daily Conversation" }
],
"pagination": {
"page": 1,
"limit": 20,
"total": 45,
"totalPages": 3,
"hasMore": true
}
}
Error
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Data tidak valid",
"details": {
"fieldErrors": {
"email": ["Email tidak valid"],
"name": ["Minimal 2 karakter"]
}
}
}
}
HTTP Status Codes
| Code | Arti | Kapan Dipakai |
|---|---|---|
200 | OK | Request berhasil (GET, PUT, PATCH) |
201 | Created | Resource berhasil dibuat (POST) |
204 | No Content | Berhasil tanpa response body (DELETE) |
400 | Bad Request | Request body tidak valid, parameter salah |
401 | Unauthorized | Belum login / session expired |
403 | Forbidden | Tidak punya akses (bukan member, bukan admin) |
404 | Not Found | Resource tidak ditemukan |
409 | Conflict | Duplikasi data (email sudah terdaftar) |
422 | Unprocessable | Data valid tapi tidak bisa diproses (kuota habis) |
429 | Too Many Requests | Rate limit terlampaui |
500 | Internal Error | Bug server (unexpected error) |
Error Codes
| Code | HTTP Status | Deskripsi |
|---|---|---|
VALIDATION_ERROR | 400 | Input tidak valid |
UNAUTHORIZED | 401 | Belum login |
FORBIDDEN | 403 | Tidak punya akses |
NOT_FOUND | 404 | Resource tidak ditemukan |
CONFLICT | 409 | Data duplikat |
QUOTA_EXCEEDED | 422 | Kuota fitur habis (upgrade ke member) |
ASSESSMENT_IN_PROGRESS | 422 | Sudah ada assessment yang belum selesai |
SESSION_EXPIRED | 401 | Session login sudah kadaluarsa |
PAYMENT_FAILED | 422 | Pembayaran gagal |
AI_SERVICE_ERROR | 503 | AI service sedang down |
RATE_LIMITED | 429 | Terlalu banyak request |
INTERNAL_ERROR | 500 | Error tidak terduga |
Authentication
ELJoy menggunakan cookie-based session yang dikelola Better Auth:
Frontend Backend
│ │
│ POST /auth/sign-in/email │
│ { email, password } ──────────►│
│ │ Verify credentials
│ │ Create session in DB
│ ◄──────────────────────────────│
│ Set-Cookie: eljoy_session=xxx │
│ │
│ GET /api/v1/learning/missions │
│ Cookie: eljoy_session=xxx ────►│
│ │ Validate session
│ │ Extract user from session
│ ◄──────────────────────────────│
│ { success: true, data: [...] } │
Protected vs Public Routes
// Public routes — tidak perlu login
app.route('/api/v1/auth', authRoutes); // Login, register
app.route('/api/v1/assessments/guest', guestAssessmentRoutes); // Ice breaker
app.route('/api/v1/content/public', publicContentRoutes); // Browse content
// Protected routes — wajib login
app.use('/api/v1/*', authMiddleware); // Apply auth check
app.route('/api/v1/users', userRoutes);
app.route('/api/v1/assessments', assessmentRoutes);
app.route('/api/v1/learning', learningRoutes);
app.route('/api/v1/conversations', conversationRoutes);
app.route('/api/v1/membership', membershipRoutes);
// Admin routes — wajib login + role admin
app.use('/api/v1/admin/*', adminMiddleware);
app.route('/api/v1/admin', adminRoutes);
Rate Limiting
| Endpoint Group | Limit | Window | Alasan |
|---|---|---|---|
| Auth (login, register) | 10 req | 15 menit | Cegah brute force |
| Assessment | 5 req | 1 menit | Cegah spam assessment |
| Conversation (voice) | 30 req | 1 menit | Normal speaking pace |
| AI Streaming | 10 req | 1 menit | Cost control |
| General API | 100 req | 1 menit | Normal usage |
| Admin API | 200 req | 1 menit | Bulk operations |
Implementasi menggunakan sliding window counter di Redis:
// Rate limit middleware
const rateLimiter = (limit: number, windowMs: number) => {
return async (c: Context, next: Next) => {
const key = `rl:${c.req.path}:${getUserId(c) || getIP(c)}`;
const current = await redis.incr(key);
if (current === 1) await redis.pexpire(key, windowMs);
if (current > limit) {
return c.json({
success: false,
error: { code: 'RATE_LIMITED', message: 'Terlalu banyak request' },
}, 429);
}
await next();
};
};
Pagination
Semua list endpoint mendukung pagination:
GET /api/v1/conversations?page=2&limit=10
| Parameter | Default | Max | Deskripsi |
|---|---|---|---|
page | 1 | - | Nomor halaman (1-indexed) |
limit | 20 | 100 | Jumlah item per halaman |
Response selalu menyertakan pagination object:
{
"pagination": {
"page": 2,
"limit": 10,
"total": 45,
"totalPages": 5,
"hasMore": true
}
}
Sorting
GET /api/v1/missions?sort=created_at&order=desc
| Parameter | Default | Options |
|---|---|---|
sort | created_at | Tergantung resource |
order | desc | asc, desc |
Filtering
Menggunakan format filter[field]=value:
GET /api/v1/missions?filter[status]=available&filter[mode]=role_play
GET /api/v1/content?filter[difficulty]=intermediate&filter[domain]=business
SSE (Server-Sent Events) Endpoints
Untuk endpoint yang mengembalikan streaming data (AI response), menggunakan SSE:
POST /api/v1/conversations/:sessionId/turn
Content-Type: multipart/form-data (audio file)
Response:
Content-Type: text/event-stream
event: transcript
data: {"text": "I think we should increase the budget"}
event: token
data: {"text": "That's"}
event: token
data: {"text": " a great"}
event: token
data: {"text": " point!"}
event: feedback
data: {"corrections": [{"word": "skedule", "suggestion": "schedule"}]}
event: audio
data: {"url": "https://r2.eljoy.id/tts/abc123.mp3"}
event: done
data: {}
File Upload
Untuk upload file audio/gambar, gunakan multipart/form-data:
POST /api/v1/conversations/:sessionId/turn
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="audio"; filename="recording.webm"
Content-Type: audio/webm
<binary audio data>
--boundary--
Batas ukuran file:
- Audio recording: max 10MB
- Profile image: max 2MB
- Content media (admin): max 20MB