.claude/skills/hono/SKILL.md
Build type-safe REST APIs with Hono framework in FTC Metrics. Use when creating API routes, implementing middleware, handling authentication, configuring CORS, or working with packages/api.
npx skillsauth add ftc8569/ftcmetrics honoInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Hono is a lightweight, ultrafast web framework for building APIs. FTC Metrics uses Hono with Node.js adapter for the backend API.
// packages/api/src/index.ts
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
const app = new Hono();
// Global middleware
app.use("*", logger());
app.use("*", cors({
origin: process.env.CORS_ORIGIN || "http://localhost:3000",
credentials: true,
}));
// Health check
app.get("/", (c) => c.json({ status: "ok" }));
// Mount routes
app.route("/api/events", eventsRouter);
app.route("/api/teams", teamsRouter);
// Start server
serve({ fetch: app.fetch, port: 3001 });
import { Hono } from "hono";
const router = new Hono();
// GET with path parameter
router.get("/:id", async (c) => {
const id = c.req.param("id");
return c.json({ success: true, data: { id } });
});
// GET with query parameters
router.get("/", async (c) => {
const page = c.req.query("page") || "1";
const limit = c.req.query("limit") || "10";
return c.json({ success: true, data: [], page, limit });
});
// POST with JSON body
router.post("/", async (c) => {
const body = await c.req.json();
const { name, value } = body;
if (!name) {
return c.json({ success: false, error: "Name required" }, 400);
}
return c.json({ success: true, data: { name, value } });
});
// PATCH for updates
router.patch("/:id", async (c) => {
const id = c.req.param("id");
const body = await c.req.json();
return c.json({ success: true, data: { id, ...body } });
});
// DELETE
router.delete("/:id", async (c) => {
const id = c.req.param("id");
return c.json({ success: true });
});
export default router;
// Route with sub-resources
router.get("/:teamId/members", async (c) => {
const teamId = c.req.param("teamId");
// Fetch members for team
return c.json({ success: true, data: members });
});
router.post("/:teamId/members", async (c) => {
const teamId = c.req.param("teamId");
const body = await c.req.json();
// Add member to team
return c.json({ success: true, data: newMember });
});
return c.json({
success: true,
data: result,
});
// 400 Bad Request
return c.json({ success: false, error: "Invalid input" }, 400);
// 401 Unauthorized
return c.json({ success: false, error: "Authentication required" }, 401);
// 403 Forbidden
return c.json({ success: false, error: "Permission denied" }, 403);
// 404 Not Found
return c.json({ success: false, error: "Resource not found" }, 404);
// 409 Conflict
return c.json({ success: false, error: "Resource already exists" }, 409);
// 500 Internal Server Error
return c.json({ success: false, error: "Internal server error" }, 500);
if (results.length === 0) {
return c.json({
success: true,
data: {
items: [],
count: 0,
},
});
}
import { Context, Next } from "hono";
import { prisma } from "@ftcmetrics/db";
export async function authMiddleware(c: Context, next: Next) {
const userId = c.req.header("X-User-Id");
if (!userId) {
return c.json({ success: false, error: "Authentication required" }, 401);
}
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, name: true, email: true },
});
if (!user) {
return c.json({ success: false, error: "Invalid user" }, 401);
}
// Attach user to context
c.set("user", user);
c.set("userId", userId);
await next();
} catch (error) {
console.error("Auth middleware error:", error);
return c.json({ success: false, error: "Authentication failed" }, 500);
}
}
export function requireTeamMembership(paramName: string = "teamId") {
return async (c: Context, next: Next) => {
const userId = c.get("userId");
const teamId = c.req.param(paramName);
if (!userId) {
return c.json({ success: false, error: "Authentication required" }, 401);
}
const membership = await prisma.teamMember.findUnique({
where: { userId_teamId: { userId, teamId } },
});
if (!membership) {
return c.json({ success: false, error: "Not a team member" }, 403);
}
c.set("membership", membership);
c.set("teamRole", membership.role);
await next();
};
}
export async function requireMentorRole(c: Context, next: Next) {
const role = c.get("teamRole");
if (role !== "MENTOR") {
return c.json({ success: false, error: "Mentor access required" }, 403);
}
await next();
}
const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
export function rateLimit(maxRequests: number = 100, windowMs: number = 60000) {
return async (c: Context, next: Next) => {
const identifier = c.req.header("X-User-Id") ||
c.req.header("X-Forwarded-For") ||
"anonymous";
const now = Date.now();
const key = `${identifier}:${c.req.path}`;
const entry = rateLimitStore.get(key);
if (!entry || now > entry.resetAt) {
rateLimitStore.set(key, { count: 1, resetAt: now + windowMs });
} else if (entry.count >= maxRequests) {
return c.json({
success: false,
error: "Rate limit exceeded",
retryAfter: Math.ceil((entry.resetAt - now) / 1000),
}, 429);
} else {
entry.count++;
}
await next();
};
}
// Apply to all routes
app.use("*", logger());
app.use("*", cors({ origin: "http://localhost:3000", credentials: true }));
// Apply to API routes only
app.use("/api/*", rateLimit(100, 60000));
app.use("/api/*", sanitizeInput);
// Single middleware
router.get("/protected", authMiddleware, async (c) => {
const user = c.get("user");
return c.json({ success: true, data: user });
});
// Chained middleware
router.patch(
"/:teamId",
authMiddleware,
requireTeamMembership("teamId"),
requireMentorRole,
async (c) => {
// Only mentors reach here
const teamId = c.req.param("teamId");
return c.json({ success: true });
}
);
router.get("/:id", async (c) => {
const id = c.req.param("id");
try {
const result = await prisma.item.findUnique({ where: { id } });
if (!result) {
return c.json({ success: false, error: "Not found" }, 404);
}
return c.json({ success: true, data: result });
} catch (error) {
console.error("Error fetching item:", error);
return c.json({ success: false, error: "Failed to fetch item" }, 500);
}
});
router.post("/", async (c) => {
try {
const body = await c.req.json();
const { teamNumber, name } = body;
// Type validation
if (!teamNumber || typeof teamNumber !== "number") {
return c.json({ success: false, error: "Team number required" }, 400);
}
// Range validation
if (teamNumber < 1 || teamNumber > 99999) {
return c.json({ success: false, error: "Invalid team number" }, 400);
}
// Parse numeric params
const parsed = parseInt(c.req.param("id"), 10);
if (isNaN(parsed)) {
return c.json({ success: false, error: "Invalid ID" }, 400);
}
// Continue with valid data
return c.json({ success: true });
} catch (error) {
return c.json({ success: false, error: "Invalid request body" }, 400);
}
});
packages/api/
src/
index.ts # Main app, server startup
routes/
analytics.ts # /api/analytics routes
events.ts # /api/events routes
scouting.ts # /api/scouting routes
teams.ts # /api/teams routes (FTC API)
user-teams.ts # /api/user-teams routes (app teams)
middleware/
auth.ts # Auth, rate limit, sanitization
lib/
ftc-api.ts # FTC Events API client
stats/ # Analytics calculations
const [matches, scores] = await Promise.all([
api.getMatches(eventCode),
api.getScores(eventCode),
]);
// Parse array from comma-separated string
const teams = c.req.query("teams")?.split(",").map(Number);
if (!teams || teams.some(isNaN)) {
return c.json({ success: false, error: "Invalid teams" }, 400);
}
// Parse enum/limited values
const level = c.req.query("level") === "playoff" ? "playoff" : "qual";
const where: Record<string, unknown> = {};
if (eventCode) {
where.eventCode = eventCode;
}
if (teamNumber) {
where.scoutedTeam = { teamNumber: parseInt(teamNumber, 10) };
}
const results = await prisma.scoutingEntry.findMany({ where });
app.all() for specific routesdevelopment
Configure TypeScript in a Bun/npm workspaces monorepo with shared base config, package-specific overrides, path aliases, and cross-package type sharing. Use when setting up tsconfig files, configuring path aliases, resolving module errors, or sharing types between packages.
development
Tailwind CSS styling for FTC Metrics with official FIRST colors and component patterns. Use when styling React components, creating responsive layouts, or implementing dark mode.
development
Configure and integrate Soketi WebSocket server with FTC Metrics for real-time updates. Use when setting up WebSocket connections, broadcasting scouting data changes, implementing presence channels for team collaboration, or debugging real-time features.
development
Create, structure, and optimize skills for the FTC Metrics project. Use when creating a new skill, improving an existing skill, or needing guidance on skill design patterns, triggers, frontmatter, and progressive disclosure.