
api
by jvidalv
apiは、other分野における実用的なスキルです。複雑な課題への対応力を強化し、業務効率と成果の質を改善します。
SKILL.md
name: api description: Use when working on the backend API (packages/api). Covers Elysia routes, Drizzle ORM, TypeBox schemas, JWT authentication, S3 uploads, Google Sheets logging, and the Next.js hybrid setup.
API Development Guide
You are working on the 100cims API (packages/api), a Next.js + Elysia hybrid backend.
Key Files
| File | Purpose |
|---|---|
src/api/routes/index.ts | Elysia app composition, error handling |
src/app/api/[[...slugs]]/route.ts | Next.js catch-all for Elysia |
src/db/schema.ts | Drizzle schema (source of truth) |
src/db/index.ts | Database client |
src/api/routes/@shared/jwt.ts | JWT middleware |
src/api/routes/@shared/s3.ts | S3 upload utilities |
src/api/lib/sheets.ts | Google Sheets logging |
drizzle.config.ts | Database connection config |
Architecture
Hybrid Stack
- Next.js 15 (App Router) for web pages and runtime
- Elysia 1.4 for API routes (mounted at
/api/*) - Drizzle ORM with PostgreSQL
- TypeBox for schema validation
Why This Hybrid?
Elysia provides excellent TypeScript inference, OpenAPI generation, and performance while Next.js handles the server runtime and potential web pages.
Directory Structure
/src/api/: All Elysia API code/routes/: Route handlers (public, protected, @shared)/schemas/: TypeBox validation schemas/lib/: Utilities (sheets, dates, images, slug)
/src/db/: Database schema and client/src/app/: Next.js pages and API catch-all route
Shared Utilities
| File | Purpose |
|---|---|
src/api/lib/slug.ts | generateSlug() - URL-friendly slug generation |
src/api/lib/images.ts | isBase64SizeValid() - Image size validation |
src/api/lib/sheets.ts | Google Sheets logging utilities |
src/api/lib/dates.ts | Date formatting utilities |
Key Patterns
Route Organization
/api/routes/
├── @shared/ # Middleware, JWT, S3, types
├── public/ # No auth required
│ ├── mountains.route.ts
│ ├── challenge.route.ts
│ └── hiscores.route.ts
├── protected/ # JWT required
│ ├── summit.route.ts
│ ├── user.route.ts
│ ├── plan.route.ts
│ ├── mountains/ # Folder-based organization
│ │ ├── index.ts
│ │ ├── my-list.route.ts
│ │ └── update.route.ts
│ └── community-challenge/
│ ├── index.ts
│ ├── create.route.ts
│ ├── update.route.ts
│ └── delete.route.ts
└── index.ts # Compose all routes
Folder-based routes: Group related endpoints in folders with an index.ts that composes them with a prefix. Each endpoint gets its own file.
Creating Routes
import { Elysia } from 'elysia';
import { db } from '@/db';
import { userSchema } from '@/api/schemas';
export const userRoute = new Elysia({ prefix: '/user', tags: ['users'] })
.get('/:id', async ({ params }) => {
const user = await db.query.user.findFirst({
where: (u, { eq }) => eq(u.id, params.id)
});
return user;
}, {
detail: { summary: 'Get user by ID' },
params: userSchema.params,
response: userSchema.response
});
Protected Routes
import { jwt } from '@/api/routes/@shared/jwt';
import { store } from '@/api/routes/@shared/store';
export const summitRoute = new Elysia({ prefix: '/summit', tags: ['summits'] })
.use(jwt)
.use(store)
.derive(async ({ bearer, store }) => {
const payload = await bearer(bearer);
store.userId = payload.userId;
})
.post('/', async ({ body, store }) => {
// store.userId available from JWT
const summit = await db.insert(summitTable).values({
userId: store.userId,
mountainId: body.mountainId
});
return summit;
});
Database Queries
import { db } from '@/db';
import { user, summit, mountain } from '@/db/schema';
import { eq, desc } from 'drizzle-orm';
// Simple query
const users = await db.select().from(user).where(eq(user.id, userId));
// Join query
const summits = await db
.select({
id: summit.id,
mountainName: mountain.name,
date: summit.createdAt
})
.from(summit)
.leftJoin(mountain, eq(summit.mountainId, mountain.id))
.where(eq(summit.userId, userId))
.orderBy(desc(summit.createdAt));
Schema Validation
import { t } from 'elysia';
export const summitSchema = {
body: t.Object({
mountainId: t.String(),
date: t.Optional(t.String()),
image: t.Optional(t.String())
}),
response: {
200: t.Object({
id: t.String(),
mountainId: t.String(),
userId: t.String()
})
}
};
Pagination Pattern
For paginated endpoints, use this pattern for backwards compatibility:
// Schema
export const PaginatedItemsSchema = t.Object({
items: t.Array(ItemSchema),
pagination: t.Object({
page: t.Number(),
pageSize: t.Number(),
totalItems: t.Number(),
totalPages: t.Number(),
hasMore: t.Boolean(),
}),
});
// Route handler - backwards compatible
const isPaginated = query.page !== undefined || query.limit !== undefined;
if (isPaginated) {
// Return paginated results with count query
return { items: results, pagination: { page, pageSize, totalItems, totalPages, hasMore } };
}
// No pagination params = return ALL results (backwards compatible)
return { items: results, pagination: { page: 1, pageSize: results.length, totalItems: results.length, totalPages: 1, hasMore: false } };
Key: Old clients without pagination params get all results. New clients can paginate.
Common Tasks
Add New Endpoint
- Create schema in
/api/schemas/ - Create route file in
/routes/public/or/protected/ - Import and use in
/routes/index.ts - Mobile app: Run
yarn generate-api-types
Database Migration
- Update
/src/db/schema.ts - Run
yarn drizzle-kit push(pushes to DB) - Verify schema changes in database
Image Upload to S3
import { putImageOnS3 } from '@/api/routes/@shared/s3';
const key = `${process.env.APP_NAME}/user/avatar/${userId}.jpeg`;
await putImageOnS3(key, buffer);
Log to Google Sheets
import { addRowToSheets, ERRORS_SPREADSHEET } from '@/api/lib/sheets';
await addRowToSheets(ERRORS_SPREADSHEET, [
'error_type',
'status_code',
'url',
'message'
]);
Environment Variables
DATABASE_URL: PostgreSQL connection stringAUTH_SECRET: JWT signing secretAWS_*: S3 credentials (region, bucket, access keys)SHEETS_*: Google service account credentialsAPP_NAME: Application name (used in S3 paths)
See .env.example for complete list.
Swagger Documentation
Available at /api/swagger during development. Auto-generated from:
- Route tags
- TypeBox schemas
- OpenAPI metadata in route definitions
Database Schema
See /src/db/schema.ts for full schema. Key tables:
user: OAuth accountsmountain: Peak data (name, lat/lng, elevation, difficulty)summit: User summit logsplan: Group hiking plansplan_attendee: Plan participantsplan_chat: Chat messageschallenge: Curated challengeshiscores: Leaderboard
Error Handling
Global error handler in /routes/index.ts:
- Logs all errors to Google Sheets
- Returns appropriate HTTP status codes
- Distinguishes ValidationError, ParseError, generic errors
Deployment
Vercel (configured in root vercel.json):
- Builds from
packages/api - Environment variables set in Vercel dashboard
- Automatic deployments on main branch
スコア
総合スコア
リポジトリの品質指標に基づく評価
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
レビュー
レビュー機能は近日公開予定です

