Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement secure API design patterns including authentication, authorization, input validation, rate limiting, and protection against common API vulnerabilities.
.claude/skills/pop123-ux-api-security-best-practices/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 177% | 0% |
| case-14 | ✓→✗ | ▼ Worse | 173% | 0% |
| case-22 | ✓→✗ | ▼ Worse | 142% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 125% | 0% |
Implement secure API design patterns including authentication, authorization, input validation, rate limiting, and protection against common API vulnerabilities.
USE WHEN:
DON'T USE WHEN:
vulnerability-scanner skill)OUTPUTS:
javascript// auth.js const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); app.post('/api/auth/login', async (req, res) => { try { const { email, password } = req.body; // Validate input if (!email || !password) { return res.status(400).json({ error: 'Email and password required' }); } // Find user const user = await db.user.findUnique({ where: { email } }); if (!user) { // Don't reveal if user exists return res.status(401).json({ error: 'Invalid credentials' }); } // Verify password const validPassword = await bcrypt.compare(password, user.passwordHash); if (!validPassword) { return res.status(401).json({ error: 'Invalid credentials' }); } // Generate JWT token const token = jwt.sign( { userId: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: '1h', issuer: 'your-app', audience: 'your-app-users' } ); // Generate refresh token const refreshToken = jwt.sign( { userId: user.id }, process.env.JWT_REFRESH_SECRET, { expiresIn: '7d' } ); // Store refresh token in database await db.refreshToken.create({ data: { token: refreshToken, userId: user.id, expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) } }); res.json({ token, refreshToken, expiresIn: 3600 }); } catch (error) { console.error('Login error:', error); res.status(500).json({ error: 'An error occurred during login' }); } });
javascript// middleware/auth.js const jwt = require('jsonwebtoken'); function authenticateToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN if (!token) { return res.status(401).json({ error: 'Access token required' }); } jwt.verify( token, process.env.JWT_SECRET, { issuer: 'your-app', audience: 'your-app-users' }, (err, user) => { if (err) { if (err.name === 'TokenExpiredError') { return res.status(401).json({ error: 'Token expired' }); } return res.status(403).json({ error: 'Invalid token' }); } req.user = user; next(); } ); } module.exports = { authenticateToken };
javascript// NEVER DO THIS - SQL Injection vulnerability app.get('/api/users/:id', async (req, res) => { const userId = req.params.id; const query = `SELECT * FROM users WHERE id = '${userId}'`; const user = await db.query(query); res.json(user); }); // Attack: GET /api/users/1' OR '1'='1 → Returns all users!
javascriptapp.get('/api/users/:id', async (req, res) => { const userId = req.params.id; // Validate input first if (!userId || !/^\d+$/.test(userId)) { return res.status(400).json({ error: 'Invalid user ID' }); } // Use parameterized query const user = await db.query( 'SELECT id, email, name FROM users WHERE id = $1', [userId] ); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); });
javascriptapp.get('/api/users/:id', async (req, res) => { const userId = parseInt(req.params.id); if (isNaN(userId)) { return res.status(400).json({ error: 'Invalid user ID' }); } const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true } // Don't select sensitive fields }); if (!user) { return res.status(404).json({ error: 'User not found' }); } res.json(user); });
javascriptconst { z } = require('zod'); const createUserSchema = z.object({ email: z.string().email('Invalid email format'), password: z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Must contain uppercase letter') .regex(/[a-z]/, 'Must contain lowercase letter') .regex(/[0-9]/, 'Must contain number'), name: z.string().min(2).max(100), age: z.number().int().min(18).max(120).optional() }); function validateRequest(schema) { return (req, res, next) => { try { schema.parse(req.body); next(); } catch (error) { res.status(400).json({ error: 'Validation failed', details: error.errors }); } }; } app.post('/api/users', validateRequest(createUserSchema), async (req, res) => { // Input is validated at this point const { email, password, name, age } = req.body; const passwordHash = await bcrypt.hash(password, 10); const user = await prisma.user.create({ data: { email, passwordHash, name, age } }); const { passwordHash: _, ...userWithoutPassword } = user; res.status(201).json(userWithoutPassword); });
javascriptconst rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); const redis = new Redis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }); // General API rate limit const apiLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: 'rl:api:' }), windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // 100 requests per window message: { error: 'Too many requests, please try again later', retryAfter: 900 }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => req.user?.userId || req.ip }); // Strict rate limit for authentication const authLimiter = rateLimit({ store: new RedisStore({ client: redis, prefix: 'rl:auth:' }), windowMs: 15 * 60 * 1000, max: 5, // Only 5 login attempts per 15 minutes skipSuccessfulRequests: true, message: { error: 'Too many login attempts, please try again later', retryAfter: 900 } }); app.use('/api/', apiLimiter); app.use('/api/auth/login', authLimiter); app.use('/api/auth/register', authLimiter);
javascriptconst helmet = require('helmet'); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", 'data:', 'https:'] } }, frameguard: { action: 'deny' }, hidePoweredBy: true, noSniff: true, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true } }));
javascriptapp.delete('/api/posts/:id', authenticateToken, async (req, res) => { await prisma.post.delete({ where: { id: req.params.id } }); res.json({ success: true }); });
javascriptapp.delete('/api/posts/:id', authenticateToken, async (req, res) => { const post = await prisma.post.findUnique({ where: { id: req.params.id } }); if (!post) { return res.status(404).json({ error: 'Post not found' }); } // Check if user owns the post or is admin if (post.userId !== req.user.userId && req.user.role !== 'admin') { return res.status(403).json({ error: 'Not authorized to delete this post' }); } await prisma.post.delete({ where: { id: req.params.id } }); res.json({ success: true }); });
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 14,764 | 13,468 | -9% | 1 | 1 | 0% | 3,322 | 6,459 | +94% | 0 | 0 | — |
case-02 | pass→pass | 11,466 | 7,022 | -39% | 1 | 1 | 0% | 2,220 | 5,006 | +125% | 0 | 0 | — |
case-03 | fail→fail | 16,112 | 10,718 | -33% | 1 | 1 | 0% | 3,175 | 5,914 | +86% | 0 | 0 | — |
case-04 | fail→fail | 20,823 | 19,576 | -6% | 1 | 1 | 0% | 4,244 | 6,640 | +56% | 0 | 0 | — |
case-05 | pass→pass | 16,902 | 14,845 | -12% | 1 | 1 | 0% | 3,383 | 6,390 | +89% | 0 | 0 | — |
case-06 | pass→pass | 12,640 | 10,911 | -14% | 1 | 1 | 0% | 2,372 | 5,761 | +143% | 0 | 0 | — |
case-07 | fail→pass | 8,596 | 7,047 | -18% | 1 | 1 | 0% | 1,812 | 5,013 | +177% | 0 | 0 | — |
case-08 | pass→pass | 12,371 | 9,350 | -24% | 1 | 1 | 0% | 2,491 | 5,442 | +118% | 0 | 0 | — |
case-09 | pass→pass | 22,142 | 6,008 | -73% | 1 | 1 | 0% | 2,271 | 4,907 | +116% | 0 | 0 | — |
case-10 | pass→pass | 12,422 | 5,504 | -56% | 1 | 1 | 0% | 2,099 | 4,672 | +123% | 0 | 0 | — |
case-11 | pass→pass | 14,557 | 8,261 | -43% | 1 | 1 | 0% | 2,960 | 5,259 | +78% | 0 | 0 | — |
case-12 | pass→pass | 7,710 | 7,204 | -7% | 1 | 1 | 0% | 1,407 | 4,752 | +238% | 0 | 0 | — |
case-13 | pass→pass | 14,187 | 8,734 | -38% | 1 | 1 | 0% | 2,268 | 5,132 | +126% | 0 | 0 | — |
case-14 | pass→fail | 10,886 | 7,408 | -32% | 1 | 1 | 0% | 1,846 | 5,038 | +173% | 0 | 0 | — |
case-15 | pass→pass | 25,592 | 11,449 | -55% | 1 | 1 | 0% | 2,252 | 5,831 | +159% | 0 | 0 | — |
case-16 | fail→fail | 12,372 | 10,666 | -14% | 1 | 1 | 0% | 2,207 | 5,795 | +163% | 0 | 0 | — |
case-17 | pass→pass | 12,522 | 9,414 | -25% | 1 | 1 | 0% | 2,438 | 5,541 | +127% | 0 | 0 | — |
case-18 | pass→pass | 15,425 | 11,809 | -23% | 1 | 1 | 0% | 2,858 | 6,029 | +111% | 0 | 0 | — |
case-19 | pass→pass | 14,872 | 9,655 | -35% | 1 | 1 | 0% | 2,752 | 5,534 | +101% | 0 | 0 | — |
case-20 | pass→pass | 5,718 | 8,108 | +42% | 1 | 1 | 0% | 1,138 | 5,310 | +367% | 0 | 0 | — |
case-21 | pass→pass | 17,181 | 15,812 | -8% | 1 | 1 | 0% | 2,794 | 6,560 | +135% | 0 | 0 | — |
case-22 | pass→fail | 11,958 | 9,278 | -22% | 1 | 1 | 0% | 2,289 | 5,539 | +142% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of 0 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.