Files
classroomio/apps/api/ROUTE_NAMING_BEST_PRACTICES.md

546 lines
14 KiB
Markdown

# API Route Naming and Structure Best Practices
This guide outlines best practices for naming and structuring API routes, inspired by Stripe's API design principles. These conventions ensure consistency, predictability, and maintainability across all API endpoints.
---
## Core Principles
### 1. **RESTful Resource Naming**
Use **plural nouns** for resource names, following REST conventions:
```typescript
// ✅ GOOD - Plural nouns
GET /courses
POST /courses
GET /courses/:id
PUT /courses/:id
DELETE /courses/:id
GET /organizations
POST /organizations
GET /organizations/:id
// ❌ BAD - Singular nouns
GET /course
POST /course
GET /organization
```
**Why?** Resources represent collections. Even when fetching a single item, you're accessing it from a collection.
---
### 2. **URL Structure Hierarchy**
Organize routes hierarchically to reflect resource relationships:
```typescript
// ✅ GOOD - Clear hierarchy
GET /organizations/:orgId/courses
POST /organizations/:orgId/courses
GET /organizations/:orgId/courses/:courseId
PUT /organizations/:orgId/courses/:courseId
GET /courses/:courseId/lessons
POST /courses/:courseId/lessons
GET /courses/:courseId/lessons/:lessonId
// ❌ BAD - Flat structure loses context
GET /courses (which org?)
GET /lessons (which course?)
```
**Exception:** Top-level resources that don't belong to a parent:
```typescript
// ✅ OK - Top-level resources
GET / account;
PUT / account / user;
GET / account / subscription;
```
---
### 3. **HTTP Method Semantics**
Use HTTP methods correctly for their intended purpose:
| Method | Purpose | Idempotent | Response Body |
| -------- | ----------------------- | ---------- | ------------------- |
| `GET` | Retrieve resource(s) | ✅ Yes | Always |
| `POST` | Create new resource | ❌ No | Created resource |
| `PUT` | Replace entire resource | ✅ Yes | Updated resource |
| `PATCH` | Partial update | ✅ Yes | Updated resource |
| `DELETE` | Remove resource | ✅ Yes | Usually empty (204) |
```typescript
// ✅ GOOD - Correct method usage
GET /courses/:id // Retrieve course
POST /courses // Create course
PUT /courses/:id // Replace entire course
PATCH /courses/:id // Partial update
DELETE /courses/:id // Delete course
// ❌ BAD - Wrong method
POST /courses/:id/update // Should be PUT or PATCH
GET /courses/:id/delete // Should be DELETE
POST /courses/:id // Should be PUT/PATCH for updates
```
---
### 4. **Nested Resources**
Nest resources when there's a clear parent-child relationship:
```typescript
// ✅ GOOD - Nested resources
GET /courses/:courseId/lessons
POST /courses/:courseId/lessons
GET /courses/:courseId/lessons/:lessonId
PUT /courses/:courseId/lessons/:lessonId
GET /organizations/:orgId/members
POST /organizations/:orgId/members
DELETE /organizations/:orgId/members/:memberId
// ❌ BAD - Flat structure with IDs
GET /lessons?courseId=123 // Less clear hierarchy
POST /lessons?courseId=123 // Relationship not obvious
```
**When to nest:**
- ✅ Child resource only exists within parent context
- ✅ Child resource ID is unique within parent (e.g., `lessonId` within `courseId`)
- ✅ Operations typically scoped to parent
**When NOT to nest:**
- ❌ Resource can exist independently
- ❌ Resource ID is globally unique
- ❌ Operations span multiple parents
```typescript
// ✅ OK - Independent resource
GET /users/:userId // User exists independently
GET /courses/:courseId // Course exists independently
// ✅ BAD - Unnecessary nesting
GET /organizations/:orgId/users/:userId // User is global, not org-specific
```
---
### 5. **Action Endpoints (Non-Resource Operations)**
For operations that don't map to CRUD, use action verbs as sub-resources:
```typescript
// ✅ GOOD - Action endpoints
POST /courses/:id/clone
POST /courses/:id/publish
POST /courses/:id/unpublish
POST /organizations/:id/invite
POST /subscriptions/:id/cancel
POST /subscriptions/:id/reactivate
// ❌ BAD - Verbs in main path
POST /clone-course/:id
POST /publish-course/:id
GET /get-courses // Redundant verb
```
**Pattern:** `POST /resource/:id/action`
**When to use:**
- ✅ State transitions (publish, unpublish, activate, cancel)
- ✅ Side effects (clone, duplicate, export)
- ✅ Complex operations that don't fit CRUD
---
### 6. **Query Parameters**
Use query parameters for filtering, sorting, pagination, and optional data:
```typescript
// ✅ GOOD - Query parameters
GET /courses?status=published
GET /courses?orgId=123&status=published
GET /courses?page=1&limit=20
GET /courses?sort=created_at&order=desc
GET /courses?search=javascript
GET /organizations?siteName=acme
// ❌ BAD - Path parameters for filters
GET /courses/status/published
GET /courses/page/1
GET /courses/search/javascript
```
**Common query parameter patterns:**
| Purpose | Parameter | Example |
| ------------- | ------------------------- | ---------------------------------- |
| Filtering | `status`, `type`, `role` | `?status=active&type=course` |
| Search | `q`, `search` | `?q=javascript` |
| Pagination | `page`, `limit`, `offset` | `?page=1&limit=20` |
| Sorting | `sort`, `order` | `?sort=created_at&order=desc` |
| Date range | `startDate`, `endDate` | `?startDate=2024-01-01` |
| Boolean flags | `include`, `expand` | `?include=lessons&expand=metadata` |
---
### 7. **URL Parameter Naming**
Use **camelCase** for URL parameters to match JavaScript conventions:
```typescript
// ✅ GOOD - camelCase
GET /courses/:courseId
GET /organizations/:orgId
GET /users/:userId
GET /lessons/:lessonId
// ❌ BAD - snake_case or kebab-case
GET /courses/:course_id
GET /courses/:course-id
```
**Exception:** When matching database column names exactly:
```typescript
// ✅ OK - Matching database convention
GET /courses/:id // Generic ID
GET /courses?siteName=acme // Matches DB column
```
---
### 8. **Consistent Response Structure**
Maintain consistent response formats across all endpoints:
```typescript
// ✅ GOOD - Consistent structure
// Single resource
{
"success": true,
"data": { ... }
}
// Collection
{
"success": true,
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}
// Error
{
"success": false,
"error": "Human-readable message",
"code": "ERROR_CODE",
"field": "fieldName" // Optional, for validation errors
}
```
---
### 9. **Versioning Strategy**
Plan for API versioning from the start:
```typescript
// Current (v1 implied)
.route('/courses', courseRouter)
// Future versioning
.route('/v1/courses', courseRouterV1)
.route('/v2/courses', courseRouterV2)
// Or use headers (Stripe style)
// X-API-Version: 2024-01-01
```
**Recommendation:** Start without versioning, add `/v1/` prefix when breaking changes are needed.
---
### 10. **Route Organization in Code**
Organize routes by domain/resource:
```typescript
// ✅ GOOD - Organized by resource
apps/api/src/routes/
├── course/
│ ├── course.ts // Main course routes
│ ├── lesson.ts // Lesson routes
│ ├── clone.ts // Clone action
│ └── index.ts // Re-exports
├── organization/
│ ├── organization.ts // Main org routes
│ ├── team.ts // Team member routes
│ └── index.ts
└── account/
├── account.ts // Account routes
└── index.ts
```
**Route registration:**
```typescript
// apps/api/src/app.ts
import { courseRouter } from '@api/routes/course';
import { organizationRouter } from '@api/routes/organization';
import { accountRouter } from '@api/routes/account';
export const app = new Hono()
.route('/course', courseRouter) // /course/*
.route('/organization', organizationRouter) // /organization/*
.route('/account', accountRouter); // /account/*
```
---
## Complete Examples
### Example 1: Course Management
```typescript
// apps/api/src/routes/course/course.ts
export const courseRouter = new Hono()
// List courses (with filters)
.get('/', authMiddleware, zValidator('query', ZListCourses), async (c) => {
// GET /course?orgId=123&status=published&page=1&limit=20
})
// Get single course
.get('/:courseId', authMiddleware, zValidator('param', ZGetCourse), async (c) => {
// GET /course/:courseId
})
// Create course
.post('/', authMiddleware, zValidator('json', ZCreateCourse), async (c) => {
// POST /course
})
// Update course (full)
.put('/:courseId', authMiddleware, zValidator('json', ZUpdateCourse), async (c) => {
// PUT /course/:courseId
})
// Update course (partial)
.patch('/:courseId', authMiddleware, zValidator('json', ZPatchCourse), async (c) => {
// PATCH /course/:courseId
})
// Delete course
.delete('/:courseId', authMiddleware, zValidator('param', ZDeleteCourse), async (c) => {
// DELETE /course/:courseId
})
// Actions
.post('/:courseId/clone', authMiddleware, zValidator('json', ZCloneCourse), async (c) => {
// POST /course/:courseId/clone
})
.post('/:courseId/publish', authMiddleware, zValidator('param', ZPublishCourse), async (c) => {
// POST /course/:courseId/publish
});
```
### Example 2: Nested Resources
```typescript
// apps/api/src/routes/course/lesson.ts
export const lessonRouter = new Hono()
// List lessons for a course
.get('/', authMiddleware, zValidator('query', ZListLessons), async (c) => {
// GET /course/:courseId/lesson?page=1&limit=20
})
// Get single lesson
.get('/:lessonId', authMiddleware, zValidator('param', ZGetLesson), async (c) => {
// GET /course/:courseId/lesson/:lessonId
})
// Create lesson
.post('/', authMiddleware, zValidator('json', ZCreateLesson), async (c) => {
// POST /course/:courseId/lesson
})
// Update lesson
.put('/:lessonId', authMiddleware, zValidator('json', ZUpdateLesson), async (c) => {
// PUT /course/:courseId/lesson/:lessonId
})
// Delete lesson
.delete('/:lessonId', authMiddleware, zValidator('param', ZDeleteLesson), async (c) => {
// DELETE /course/:courseId/lesson/:lessonId
});
// Register nested route
// apps/api/src/routes/course/course.ts
export const courseRouter = new Hono()
// ... course routes ...
.route('/:courseId/lesson', lessonRouter); // Nested under course
```
### Example 3: Organization with Team
```typescript
// apps/api/src/routes/organization/organization.ts
export const organizationRouter = new Hono()
// List organizations
.get('/', authMiddleware, zValidator('query', ZListOrgs), async (c) => {
// GET /organization?siteName=acme
})
// Get organization
.get('/:orgId', authMiddleware, zValidator('param', ZGetOrg), async (c) => {
// GET /organization/:orgId
})
// Create organization
.post('/', authMiddleware, zValidator('json', ZCreateOrg), async (c) => {
// POST /organization
})
// Update organization
.put('/:orgId', authMiddleware, zValidator('json', ZUpdateOrg), async (c) => {
// PUT /organization/:orgId
})
// Team management (nested)
.get('/:orgId/team', authMiddleware, zValidator('param', ZGetTeam), async (c) => {
// GET /organization/:orgId/team
})
.post('/:orgId/team/invite', authMiddleware, zValidator('json', ZInviteTeam), async (c) => {
// POST /organization/:orgId/team/invite
})
.delete('/:orgId/team/:memberId', authMiddleware, zValidator('param', ZRemoveMember), async (c) => {
// DELETE /organization/:orgId/team/:memberId
});
```
---
## Anti-Patterns to Avoid
### ❌ Don't: Use verbs in resource names
```typescript
// ❌ BAD
GET /get-courses
POST /create-course
POST /update-course/:id
GET /delete-course/:id
// ✅ GOOD
GET /courses
POST /courses
PUT /courses/:id
DELETE /courses/:id
```
### ❌ Don't: Mix singular and plural
```typescript
// ❌ BAD
GET / course;
POST / courses;
GET / organization;
POST / organizations;
// ✅ GOOD
GET / courses;
POST / courses;
GET / organizations;
POST / organizations;
```
### ❌ Don't: Use query parameters for required identifiers
```typescript
// ❌ BAD
GET /courses?id=123
GET /courses?courseId=123
// ✅ GOOD
GET /courses/:courseId
GET /courses/123
```
### ❌ Don't: Create deep nesting (>3 levels)
```typescript
// ❌ BAD - Too deep
GET /organizations/:orgId/courses/:courseId/lessons/:lessonId/exercises/:exerciseId
// ✅ GOOD - Flatten when appropriate
GET /exercises/:exerciseId?courseId=123&lessonId=456
// Or use a different resource structure
GET /lessons/:lessonId/exercises/:exerciseId
```
### ❌ Don't: Use inconsistent naming
```typescript
// ❌ BAD - Mixed conventions
GET /courses/:course_id
GET /organizations/:orgId
GET /users/:userId
// ✅ GOOD - Consistent camelCase
GET /courses/:courseId
GET /organizations/:orgId
GET /users/:userId
```
---
## Checklist for New Routes
When creating a new route, ensure:
- [ ] Resource name is **plural** (e.g., `/courses`, not `/course`)
- [ ] URL parameters use **camelCase** (e.g., `:courseId`, not `:course_id`)
- [ ] HTTP method matches operation (GET=read, POST=create, PUT=replace, PATCH=update, DELETE=remove)
- [ ] Nested resources reflect parent-child relationships
- [ ] Action endpoints use `POST /resource/:id/action` pattern
- [ ] Query parameters used for filtering, pagination, sorting
- [ ] Route is registered in main `app.ts`
- [ ] Validation schema exists in `packages/utils/src/validation/`
- [ ] Route follows existing patterns in codebase
- [ ] Response format is consistent with other routes
---
## Summary
**Key Takeaways:**
1. **Plural nouns** for resources (`/courses`, `/organizations`)
2. **camelCase** for parameters (`:courseId`, `:orgId`)
3. **Hierarchical nesting** for related resources
4. **HTTP methods** used semantically (GET, POST, PUT, PATCH, DELETE)
5. **Action verbs** as sub-resources for non-CRUD operations
6. **Query parameters** for filtering, pagination, sorting
7. **Consistent structure** across all endpoints
8. **Organized by domain** in code structure
Following these conventions ensures your API is **predictable**, **maintainable**, and **developer-friendly**.