Skip to main content

04 — API Design & Conventions

Dokumen ini menjelaskan aturan dan konvensi yang berlaku untuk semua endpoint API ELJoy.


Base URL​

EnvironmentBase URL
Developmenthttp://localhost:3001/api/v1
Staginghttps://api-staging.eljoy.id/api/v1
Productionhttps://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​

CodeArtiKapan Dipakai
200OKRequest berhasil (GET, PUT, PATCH)
201CreatedResource berhasil dibuat (POST)
204No ContentBerhasil tanpa response body (DELETE)
400Bad RequestRequest body tidak valid, parameter salah
401UnauthorizedBelum login / session expired
403ForbiddenTidak punya akses (bukan member, bukan admin)
404Not FoundResource tidak ditemukan
409ConflictDuplikasi data (email sudah terdaftar)
422UnprocessableData valid tapi tidak bisa diproses (kuota habis)
429Too Many RequestsRate limit terlampaui
500Internal ErrorBug server (unexpected error)

Error Codes​

CodeHTTP StatusDeskripsi
VALIDATION_ERROR400Input tidak valid
UNAUTHORIZED401Belum login
FORBIDDEN403Tidak punya akses
NOT_FOUND404Resource tidak ditemukan
CONFLICT409Data duplikat
QUOTA_EXCEEDED422Kuota fitur habis (upgrade ke member)
ASSESSMENT_IN_PROGRESS422Sudah ada assessment yang belum selesai
SESSION_EXPIRED401Session login sudah kadaluarsa
PAYMENT_FAILED422Pembayaran gagal
AI_SERVICE_ERROR503AI service sedang down
RATE_LIMITED429Terlalu banyak request
INTERNAL_ERROR500Error 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 GroupLimitWindowAlasan
Auth (login, register)10 req15 menitCegah brute force
Assessment5 req1 menitCegah spam assessment
Conversation (voice)30 req1 menitNormal speaking pace
AI Streaming10 req1 menitCost control
General API100 req1 menitNormal usage
Admin API200 req1 menitBulk 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
ParameterDefaultMaxDeskripsi
page1-Nomor halaman (1-indexed)
limit20100Jumlah 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
ParameterDefaultOptions
sortcreated_atTergantung resource
orderdescasc, 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