Loading...
Loading...
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.
npx skill4agent add jvidalv/100cims apipackages/api| File | Purpose |
|---|---|
| Elysia app composition, error handling |
| Next.js catch-all for Elysia |
| Drizzle schema (source of truth) |
| Database client |
| JWT middleware |
| S3 upload utilities |
| Google Sheets logging |
| Database connection config |
/api/*/src/api//routes//schemas//lib//src/db//src/app/| File | Purpose |
|---|---|
| |
| |
| Google Sheets logging utilities |
| Date formatting utilities |
/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 routesindex.tsimport { 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
});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;
});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));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()
})
}
};// 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 } };/api/schemas//routes/public//protected//routes/index.tsyarn generate-api-types/src/db/schema.tsyarn drizzle-kit pushimport { putImageOnS3 } from '@/api/routes/@shared/s3';
const key = `${process.env.APP_NAME}/user/avatar/${userId}.jpeg`;
await putImageOnS3(key, buffer);import { addRowToSheets, ERRORS_SPREADSHEET } from '@/api/lib/sheets';
await addRowToSheets(ERRORS_SPREADSHEET, [
'error_type',
'status_code',
'url',
'message'
]);DATABASE_URLAUTH_SECRETAWS_*SHEETS_*APP_NAME.env.example/api/swagger/src/db/schema.tsusermountainsummitplanplan_attendeeplan_chatchallengehiscores/routes/index.tsvercel.jsonpackages/api