From 1fc3685dbae382d37cddd66773da6a6750c8763b Mon Sep 17 00:00:00 2001 From: njg7194 Date: Sun, 1 Feb 2026 13:22:09 +0900 Subject: [PATCH] feat: add API framework skills (GraphQL, REST, gRPC) - GraphQL: schema design, queries, mutations, subscriptions, auth, performance - REST API: HTTP methods, OpenAPI spec, versioning, rate limiting - gRPC: protobuf, streaming, authentication, error handling, performance These skills provide comprehensive guidance for building and interacting with modern APIs. --- skills/graphql/SKILL.md | 125 +++++++++++ skills/graphql/references/auth.md | 155 ++++++++++++++ skills/graphql/references/introspection.md | 125 +++++++++++ skills/graphql/references/performance.md | 186 +++++++++++++++++ skills/graphql/references/subscriptions.md | 129 ++++++++++++ skills/grpc/SKILL.md | 189 +++++++++++++++++ skills/grpc/references/auth.md | 168 +++++++++++++++ skills/grpc/references/errors.md | 179 ++++++++++++++++ skills/grpc/references/performance.md | 204 ++++++++++++++++++ skills/grpc/references/streaming.md | 219 ++++++++++++++++++++ skills/rest-api/SKILL.md | 149 +++++++++++++ skills/rest-api/references/auth.md | 113 ++++++++++ skills/rest-api/references/openapi.md | 200 ++++++++++++++++++ skills/rest-api/references/rate-limiting.md | 160 ++++++++++++++ skills/rest-api/references/versioning.md | 137 ++++++++++++ 15 files changed, 2438 insertions(+) create mode 100644 skills/graphql/SKILL.md create mode 100644 skills/graphql/references/auth.md create mode 100644 skills/graphql/references/introspection.md create mode 100644 skills/graphql/references/performance.md create mode 100644 skills/graphql/references/subscriptions.md create mode 100644 skills/grpc/SKILL.md create mode 100644 skills/grpc/references/auth.md create mode 100644 skills/grpc/references/errors.md create mode 100644 skills/grpc/references/performance.md create mode 100644 skills/grpc/references/streaming.md create mode 100644 skills/rest-api/SKILL.md create mode 100644 skills/rest-api/references/auth.md create mode 100644 skills/rest-api/references/openapi.md create mode 100644 skills/rest-api/references/rate-limiting.md create mode 100644 skills/rest-api/references/versioning.md diff --git a/skills/graphql/SKILL.md b/skills/graphql/SKILL.md new file mode 100644 index 0000000..35f575c --- /dev/null +++ b/skills/graphql/SKILL.md @@ -0,0 +1,125 @@ +--- +name: graphql +version: 1.0.0 +description: GraphQL API development and interaction skill. Use when building GraphQL schemas, writing queries/mutations, introspecting APIs, or debugging GraphQL endpoints. +argument-hint: "[query|mutation|introspect] [endpoint] [--variables]" +--- + +# GraphQL Skill + +Build, query, and debug GraphQL APIs effectively. + +## Quick Reference + +```bash +# Introspect a GraphQL endpoint +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"query": "{ __schema { types { name } } }"}' + +# Execute a query +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"query": "query { users { id name } }"}' + +# Execute a mutation with variables +curl -X POST \ + -H "Content-Type: application/json" \ + -d '{"query": "mutation($input: CreateUserInput!) { createUser(input: $input) { id } }", "variables": {"input": {"name": "John"}}}' +``` + +## Schema Design Patterns + +### Type Definitions +```graphql +type User { + id: ID! + name: String! + email: String! + posts: [Post!]! + createdAt: DateTime! +} + +type Post { + id: ID! + title: String! + content: String! + author: User! +} + +input CreateUserInput { + name: String! + email: String! +} + +type Query { + user(id: ID!): User + users(first: Int, after: String): UserConnection! +} + +type Mutation { + createUser(input: CreateUserInput!): User! + updateUser(id: ID!, input: UpdateUserInput!): User! + deleteUser(id: ID!): Boolean! +} +``` + +### Pagination (Relay-style) +```graphql +type UserConnection { + edges: [UserEdge!]! + pageInfo: PageInfo! + totalCount: Int! +} + +type UserEdge { + node: User! + cursor: String! +} + +type PageInfo { + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + endCursor: String +} +``` + +## Best Practices + +| Practice | Description | +|----------|-------------| +| **Nullable by default** | Only use `!` when field is guaranteed non-null | +| **Use Input types** | Wrap mutation arguments in Input types | +| **Descriptive names** | `createUser`, not `addUser` or `newUser` | +| **Relay pagination** | Use Connection pattern for lists | +| **Error handling** | Use union types for error states | +| **Batching** | Use DataLoader to prevent N+1 queries | + +## Error Handling Pattern + +```graphql +union CreateUserResult = User | ValidationError | AuthError + +type ValidationError { + field: String! + message: String! +} + +type AuthError { + message: String! +} + +type Mutation { + createUser(input: CreateUserInput!): CreateUserResult! +} +``` + +## References + +| Topic | File | +|-------|------| +| Introspection | [introspection.md](references/introspection.md) | +| Subscriptions | [subscriptions.md](references/subscriptions.md) | +| Authentication | [auth.md](references/auth.md) | +| Performance | [performance.md](references/performance.md) | diff --git a/skills/graphql/references/auth.md b/skills/graphql/references/auth.md new file mode 100644 index 0000000..b83b0d2 --- /dev/null +++ b/skills/graphql/references/auth.md @@ -0,0 +1,155 @@ +# GraphQL Authentication & Authorization + +## Authentication Methods + +### Bearer Token (JWT) + +```bash +curl -X POST https://api.example.com/graphql \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ + -d '{"query": "{ me { id name } }"}' +``` + +### API Key + +```bash +curl -X POST https://api.example.com/graphql \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-api-key" \ + -d '{"query": "{ users { id } }"}' +``` + +## Server-Side Context + +```javascript +const server = new ApolloServer({ + typeDefs, + resolvers, + context: async ({ req }) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (token) { + try { + const user = await verifyToken(token); + return { user }; + } catch (e) { + // Invalid token - continue as unauthenticated + } + } + + return { user: null }; + }, +}); +``` + +## Authorization Patterns + +### Field-Level Authorization + +```javascript +const resolvers = { + User: { + email: (user, _, { currentUser }) => { + // Only show email to the user themselves or admins + if (currentUser?.id === user.id || currentUser?.role === 'ADMIN') { + return user.email; + } + return null; + }, + }, +}; +``` + +### Directive-Based + +```graphql +directive @auth(requires: Role = USER) on FIELD_DEFINITION + +enum Role { + ADMIN + USER + GUEST +} + +type Query { + users: [User!]! @auth(requires: ADMIN) + me: User @auth(requires: USER) + publicPosts: [Post!]! +} +``` + +```javascript +class AuthDirective extends SchemaDirectiveVisitor { + visitFieldDefinition(field) { + const { requires } = this.args; + const originalResolve = field.resolve; + + field.resolve = async function (...args) { + const context = args[2]; + + if (!context.user) { + throw new AuthenticationError('Not authenticated'); + } + + if (!hasRole(context.user, requires)) { + throw new ForbiddenError('Not authorized'); + } + + return originalResolve.apply(this, args); + }; + } +} +``` + +### Shield Library + +```javascript +import { shield, rule, allow, deny } from 'graphql-shield'; + +const isAuthenticated = rule()((parent, args, { user }) => !!user); +const isAdmin = rule()((parent, args, { user }) => user?.role === 'ADMIN'); +const isOwner = rule()((parent, args, { user }) => parent.userId === user?.id); + +const permissions = shield({ + Query: { + users: isAdmin, + me: isAuthenticated, + publicPosts: allow, + }, + Mutation: { + createPost: isAuthenticated, + deleteUser: isAdmin, + }, + User: { + email: or(isOwner, isAdmin), + }, +}); +``` + +## Error Handling + +```javascript +import { AuthenticationError, ForbiddenError } from 'apollo-server'; + +// In resolvers +if (!context.user) { + throw new AuthenticationError('You must be logged in'); +} + +if (context.user.role !== 'ADMIN') { + throw new ForbiddenError('Admin access required'); +} +``` + +Response format: +```json +{ + "errors": [{ + "message": "You must be logged in", + "extensions": { + "code": "UNAUTHENTICATED" + } + }] +} +``` diff --git a/skills/graphql/references/introspection.md b/skills/graphql/references/introspection.md new file mode 100644 index 0000000..6252a18 --- /dev/null +++ b/skills/graphql/references/introspection.md @@ -0,0 +1,125 @@ +# GraphQL Introspection + +## Full Schema Introspection + +```graphql +query IntrospectionQuery { + __schema { + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + locations + args { + ...InputValue + } + } + } +} + +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} + +fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue +} + +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } +} +``` + +## Quick Type Lookup + +```graphql +# List all types +query { __schema { types { name kind } } } + +# Get specific type details +query { __type(name: "User") { + name + fields { name type { name kind } } +}} + +# Get all queries +query { __schema { queryType { fields { name } } } } + +# Get all mutations +query { __schema { mutationType { fields { name } } } } +``` + +## cURL Examples + +```bash +# Get all type names +curl -s -X POST https://api.example.com/graphql \ + -H "Content-Type: application/json" \ + -d '{"query": "{ __schema { types { name } } }"}' | python3 -m json.tool + +# Get User type fields +curl -s -X POST https://api.example.com/graphql \ + -H "Content-Type: application/json" \ + -d '{"query": "{ __type(name: \"User\") { fields { name type { name } } } }"}' +``` + +## Tools + +- **GraphQL Playground**: Interactive IDE +- **GraphiQL**: In-browser GraphQL IDE +- **Insomnia**: API client with GraphQL support +- **Apollo Studio**: Schema management and monitoring diff --git a/skills/graphql/references/performance.md b/skills/graphql/references/performance.md new file mode 100644 index 0000000..1bcf1a6 --- /dev/null +++ b/skills/graphql/references/performance.md @@ -0,0 +1,186 @@ +# GraphQL Performance Optimization + +## N+1 Query Problem + +### The Problem + +```graphql +query { + posts { # 1 query + author { # N queries (one per post) + name + } + } +} +``` + +### Solution: DataLoader + +```javascript +import DataLoader from 'dataloader'; + +// Create loader +const userLoader = new DataLoader(async (userIds) => { + const users = await User.findByIds(userIds); + const userMap = new Map(users.map(u => [u.id, u])); + return userIds.map(id => userMap.get(id)); +}); + +// In resolver +const resolvers = { + Post: { + author: (post, _, { loaders }) => { + return loaders.user.load(post.authorId); + }, + }, +}; + +// Context setup (new loader per request) +const context = () => ({ + loaders: { + user: new DataLoader(batchUsers), + }, +}); +``` + +## Query Complexity Analysis + +```javascript +import { createComplexityLimitRule } from 'graphql-validation-complexity'; + +const complexityRule = createComplexityLimitRule(1000, { + scalarCost: 1, + objectCost: 10, + listFactor: 20, + introspectionListFactor: 2, +}); + +const server = new ApolloServer({ + typeDefs, + resolvers, + validationRules: [complexityRule], +}); +``` + +### Manual Complexity + +```graphql +type Query { + users(first: Int!): [User!]! @complexity(value: 10, multipliers: ["first"]) +} +``` + +## Query Depth Limiting + +```javascript +import depthLimit from 'graphql-depth-limit'; + +const server = new ApolloServer({ + typeDefs, + resolvers, + validationRules: [depthLimit(10)], +}); +``` + +## Caching + +### Response Caching (Apollo) + +```graphql +type Query { + user(id: ID!): User @cacheControl(maxAge: 60) +} + +type User @cacheControl(maxAge: 120) { + id: ID! + name: String! + email: String! @cacheControl(maxAge: 0) # Never cache +} +``` + +### Persisted Queries + +```javascript +// Client sends hash instead of full query +{ + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "abc123..." + } + }, + "variables": { "id": "1" } +} +``` + +## Pagination Best Practices + +### Cursor-based (Recommended) + +```graphql +query { + users(first: 20, after: "cursor123") { + edges { + node { id name } + cursor + } + pageInfo { + hasNextPage + endCursor + } + } +} +``` + +### Limit page size + +```javascript +const resolvers = { + Query: { + users: (_, { first = 20 }) => { + const limit = Math.min(first, 100); // Cap at 100 + return User.findMany({ take: limit }); + }, + }, +}; +``` + +## Field Selection Optimization + +```javascript +import graphqlFields from 'graphql-fields'; + +const resolvers = { + Query: { + users: (_, args, context, info) => { + const requestedFields = graphqlFields(info); + + // Only select requested columns + const select = Object.keys(requestedFields); + return User.findMany({ select }); + }, + }, +}; +``` + +## Monitoring + +- **Apollo Studio**: Built-in tracing and analytics +- **OpenTelemetry**: Distributed tracing +- **Custom plugins**: Log slow queries + +```javascript +const slowQueryPlugin = { + requestDidStart() { + const start = Date.now(); + return { + willSendResponse({ request }) { + const duration = Date.now() - start; + if (duration > 1000) { + console.warn(`Slow query (${duration}ms):`, request.query); + } + }, + }; + }, +}; +``` diff --git a/skills/graphql/references/subscriptions.md b/skills/graphql/references/subscriptions.md new file mode 100644 index 0000000..6d5b95a --- /dev/null +++ b/skills/graphql/references/subscriptions.md @@ -0,0 +1,129 @@ +# GraphQL Subscriptions + +## Schema Definition + +```graphql +type Subscription { + messageAdded(channelId: ID!): Message! + userStatusChanged(userId: ID!): UserStatus! + orderUpdated(orderId: ID!): Order! +} + +type Message { + id: ID! + content: String! + author: User! + createdAt: DateTime! +} +``` + +## Client Implementation (JavaScript) + +### Using graphql-ws + +```javascript +import { createClient } from 'graphql-ws'; + +const client = createClient({ + url: 'wss://api.example.com/graphql', + connectionParams: { + authToken: 'your-token', + }, +}); + +// Subscribe +const unsubscribe = client.subscribe( + { + query: `subscription ($channelId: ID!) { + messageAdded(channelId: $channelId) { + id + content + author { name } + } + }`, + variables: { channelId: '123' }, + }, + { + next: (data) => console.log('New message:', data), + error: (err) => console.error('Error:', err), + complete: () => console.log('Subscription complete'), + } +); + +// Later: unsubscribe +unsubscribe(); +``` + +### Using Apollo Client + +```javascript +import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'; +import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; +import { getMainDefinition } from '@apollo/client/utilities'; +import { createClient } from 'graphql-ws'; + +const httpLink = new HttpLink({ uri: 'https://api.example.com/graphql' }); + +const wsLink = new GraphQLWsLink(createClient({ + url: 'wss://api.example.com/graphql', +})); + +const splitLink = split( + ({ query }) => { + const definition = getMainDefinition(query); + return ( + definition.kind === 'OperationDefinition' && + definition.operation === 'subscription' + ); + }, + wsLink, + httpLink, +); + +const client = new ApolloClient({ + link: splitLink, + cache: new InMemoryCache(), +}); +``` + +## Server Implementation (Node.js) + +```javascript +import { createServer } from 'http'; +import { WebSocketServer } from 'ws'; +import { useServer } from 'graphql-ws/lib/use/ws'; +import { schema } from './schema'; +import { PubSub } from 'graphql-subscriptions'; + +const pubsub = new PubSub(); + +const resolvers = { + Subscription: { + messageAdded: { + subscribe: (_, { channelId }) => + pubsub.asyncIterator([`MESSAGE_ADDED_${channelId}`]), + }, + }, + Mutation: { + sendMessage: async (_, { channelId, content }, { user }) => { + const message = await createMessage({ channelId, content, authorId: user.id }); + pubsub.publish(`MESSAGE_ADDED_${channelId}`, { messageAdded: message }); + return message; + }, + }, +}; + +const server = createServer(); +const wsServer = new WebSocketServer({ server, path: '/graphql' }); +useServer({ schema }, wsServer); + +server.listen(4000); +``` + +## Best Practices + +1. **Use Redis PubSub** for production (not in-memory PubSub) +2. **Implement connection authentication** via `connectionParams` +3. **Handle reconnection** on the client side +4. **Rate limit subscriptions** to prevent abuse +5. **Use filters** to send only relevant updates diff --git a/skills/grpc/SKILL.md b/skills/grpc/SKILL.md new file mode 100644 index 0000000..cabc524 --- /dev/null +++ b/skills/grpc/SKILL.md @@ -0,0 +1,189 @@ +--- +name: grpc +version: 1.0.0 +description: gRPC API development and interaction skill. Use when building gRPC services, defining protobuf schemas, debugging gRPC endpoints, or implementing streaming APIs. +argument-hint: "[service.method] [endpoint] [--message]" +--- + +# gRPC Skill + +Build high-performance RPC services with Protocol Buffers. + +## Quick Reference + +### Using grpcurl + +```bash +# List services +grpcurl -plaintext localhost:50051 list + +# Describe service +grpcurl -plaintext localhost:50051 describe user.UserService + +# Call unary method +grpcurl -plaintext -d '{"id": "123"}' \ + localhost:50051 user.UserService/GetUser + +# Call with JSON file +grpcurl -plaintext -d @ localhost:50051 user.UserService/CreateUser < request.json + +# With TLS +grpcurl api.example.com:443 user.UserService/GetUser + +# With auth token +grpcurl -H "Authorization: Bearer " \ + localhost:50051 user.UserService/GetUser +``` + +### Using grpc_cli + +```bash +grpc_cli call localhost:50051 GetUser "id: '123'" +grpc_cli ls localhost:50051 +``` + +## Protobuf Basics + +### Service Definition + +```protobuf +syntax = "proto3"; + +package user; + +option go_package = "github.com/example/user"; + +// Service definition +service UserService { + // Unary RPC + rpc GetUser(GetUserRequest) returns (User); + + // Server streaming + rpc ListUsers(ListUsersRequest) returns (stream User); + + // Client streaming + rpc UploadUsers(stream User) returns (UploadResponse); + + // Bidirectional streaming + rpc Chat(stream ChatMessage) returns (stream ChatMessage); +} + +// Messages +message User { + string id = 1; + string name = 2; + string email = 3; + UserStatus status = 4; + google.protobuf.Timestamp created_at = 5; + + // Nested message + Address address = 6; +} + +message Address { + string street = 1; + string city = 2; + string country = 3; +} + +enum UserStatus { + USER_STATUS_UNSPECIFIED = 0; + USER_STATUS_ACTIVE = 1; + USER_STATUS_INACTIVE = 2; +} + +message GetUserRequest { + string id = 1; +} + +message ListUsersRequest { + int32 page_size = 1; + string page_token = 2; +} +``` + +## RPC Types + +| Type | Client | Server | Use Case | +|------|--------|--------|----------| +| **Unary** | 1 request | 1 response | Simple request/response | +| **Server Stream** | 1 request | N responses | Large data download | +| **Client Stream** | N requests | 1 response | File upload | +| **Bidirectional** | N requests | N responses | Chat, real-time | + +## Error Handling + +```protobuf +import "google/rpc/status.proto"; + +// Standard gRPC status codes +// OK = 0 +// CANCELLED = 1 +// UNKNOWN = 2 +// INVALID_ARGUMENT = 3 +// DEADLINE_EXCEEDED = 4 +// NOT_FOUND = 5 +// ALREADY_EXISTS = 6 +// PERMISSION_DENIED = 7 +// RESOURCE_EXHAUSTED = 8 +// FAILED_PRECONDITION = 9 +// ABORTED = 10 +// OUT_OF_RANGE = 11 +// UNIMPLEMENTED = 12 +// INTERNAL = 13 +// UNAVAILABLE = 14 +// DATA_LOSS = 15 +// UNAUTHENTICATED = 16 +``` + +```go +// Go example +import "google.golang.org/grpc/status" + +func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) { + user, err := s.db.FindUser(req.Id) + if err != nil { + return nil, status.Errorf(codes.NotFound, "user %s not found", req.Id) + } + return user, nil +} +``` + +## Code Generation + +```bash +# Install protoc +brew install protobuf # macOS +apt install protobuf-compiler # Ubuntu + +# Install language plugins +go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + +# Generate code +protoc --go_out=. --go-grpc_out=. proto/*.proto + +# Python +pip install grpcio-tools +python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. proto/*.proto +``` + +## Best Practices + +| Practice | Description | +|----------|-------------| +| **Use proto3** | Modern syntax, better defaults | +| **Reserve fields** | Don't reuse field numbers | +| **Meaningful names** | ServiceName/MethodName pattern | +| **Deadlines** | Always set client deadlines | +| **Interceptors** | Use for logging, auth, metrics | +| **Keep messages small** | Stream for large data | + +## References + +| Topic | File | +|-------|------| +| Streaming | [streaming.md](references/streaming.md) | +| Authentication | [auth.md](references/auth.md) | +| Error Handling | [errors.md](references/errors.md) | +| Performance | [performance.md](references/performance.md) | diff --git a/skills/grpc/references/auth.md b/skills/grpc/references/auth.md new file mode 100644 index 0000000..c829b2b --- /dev/null +++ b/skills/grpc/references/auth.md @@ -0,0 +1,168 @@ +# gRPC Authentication + +## Authentication Methods + +### 1. Token-Based (JWT/Bearer) + +#### Client +```go +// Per-call credentials +type tokenAuth struct { + token string +} + +func (t tokenAuth) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { + return map[string]string{ + "authorization": "Bearer " + t.token, + }, nil +} + +func (t tokenAuth) RequireTransportSecurity() bool { + return true +} + +// Usage +conn, err := grpc.Dial(address, + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithPerRPCCredentials(tokenAuth{token: "your-jwt-token"}), +) +``` + +#### Server Interceptor +```go +func authInterceptor( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, +) (interface{}, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "missing metadata") + } + + auth := md.Get("authorization") + if len(auth) == 0 { + return nil, status.Error(codes.Unauthenticated, "missing auth token") + } + + token := strings.TrimPrefix(auth[0], "Bearer ") + user, err := validateToken(token) + if err != nil { + return nil, status.Error(codes.Unauthenticated, "invalid token") + } + + // Add user to context + ctx = context.WithValue(ctx, "user", user) + return handler(ctx, req) +} + +// Register +server := grpc.NewServer( + grpc.UnaryInterceptor(authInterceptor), +) +``` + +### 2. mTLS (Mutual TLS) + +```go +// Server +cert, _ := tls.LoadX509KeyPair("server.crt", "server.key") +certPool := x509.NewCertPool() +ca, _ := ioutil.ReadFile("ca.crt") +certPool.AppendCertsFromPEM(ca) + +creds := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, +}) + +server := grpc.NewServer(grpc.Creds(creds)) + +// Client +cert, _ := tls.LoadX509KeyPair("client.crt", "client.key") +certPool := x509.NewCertPool() +ca, _ := ioutil.ReadFile("ca.crt") +certPool.AppendCertsFromPEM(ca) + +creds := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: certPool, +}) + +conn, _ := grpc.Dial(address, grpc.WithTransportCredentials(creds)) +``` + +### 3. API Key + +```go +// Client metadata +ctx := metadata.AppendToOutgoingContext(ctx, "x-api-key", "your-api-key") +response, err := client.GetUser(ctx, request) + +// Server extraction +md, _ := metadata.FromIncomingContext(ctx) +apiKey := md.Get("x-api-key") +``` + +## grpcurl with Auth + +```bash +# Bearer token +grpcurl -H "Authorization: Bearer " \ + localhost:50051 user.UserService/GetUser + +# API Key +grpcurl -H "X-API-Key: your-key" \ + localhost:50051 user.UserService/GetUser + +# mTLS +grpcurl -cert client.crt -key client.key -cacert ca.crt \ + api.example.com:443 user.UserService/GetUser +``` + +## Streaming Interceptor + +```go +func streamAuthInterceptor( + srv interface{}, + ss grpc.ServerStream, + info *grpc.StreamServerInfo, + handler grpc.StreamHandler, +) error { + md, ok := metadata.FromIncomingContext(ss.Context()) + if !ok { + return status.Error(codes.Unauthenticated, "missing metadata") + } + + // Validate auth... + + // Wrap stream with new context + wrapped := &wrappedStream{ + ServerStream: ss, + ctx: context.WithValue(ss.Context(), "user", user), + } + + return handler(srv, wrapped) +} + +type wrappedStream struct { + grpc.ServerStream + ctx context.Context +} + +func (w *wrappedStream) Context() context.Context { + return w.ctx +} +``` + +## Best Practices + +| Practice | Description | +|----------|-------------| +| **Always use TLS** | Never plaintext in production | +| **Rotate tokens** | Short-lived access tokens | +| **Validate on server** | Never trust client data | +| **Rate limit** | Prevent abuse | +| **Audit logging** | Log auth events | diff --git a/skills/grpc/references/errors.md b/skills/grpc/references/errors.md new file mode 100644 index 0000000..54d8be3 --- /dev/null +++ b/skills/grpc/references/errors.md @@ -0,0 +1,179 @@ +# gRPC Error Handling + +## Standard Status Codes + +| Code | Name | When to Use | +|------|------|-------------| +| 0 | OK | Success | +| 1 | CANCELLED | Operation cancelled by client | +| 2 | UNKNOWN | Unknown error | +| 3 | INVALID_ARGUMENT | Bad request parameters | +| 4 | DEADLINE_EXCEEDED | Timeout | +| 5 | NOT_FOUND | Resource doesn't exist | +| 6 | ALREADY_EXISTS | Resource already exists | +| 7 | PERMISSION_DENIED | No permission (authenticated) | +| 8 | RESOURCE_EXHAUSTED | Rate limit, quota exceeded | +| 9 | FAILED_PRECONDITION | System not in required state | +| 10 | ABORTED | Operation aborted (concurrency) | +| 11 | OUT_OF_RANGE | Invalid range (pagination) | +| 12 | UNIMPLEMENTED | Method not implemented | +| 13 | INTERNAL | Internal server error | +| 14 | UNAVAILABLE | Service temporarily unavailable | +| 15 | DATA_LOSS | Unrecoverable data loss | +| 16 | UNAUTHENTICATED | Not authenticated | + +## Returning Errors + +### Go +```go +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) { + // Validation error + if req.Id == "" { + return nil, status.Errorf(codes.InvalidArgument, "user id is required") + } + + // Not found + user, err := s.db.FindUser(req.Id) + if err == ErrNotFound { + return nil, status.Errorf(codes.NotFound, "user %s not found", req.Id) + } + + // Internal error + if err != nil { + return nil, status.Errorf(codes.Internal, "database error: %v", err) + } + + return user, nil +} +``` + +### Python +```python +import grpc + +def GetUser(self, request, context): + if not request.id: + context.abort(grpc.StatusCode.INVALID_ARGUMENT, "user id is required") + + user = self.db.find_user(request.id) + if not user: + context.abort(grpc.StatusCode.NOT_FOUND, f"user {request.id} not found") + + return user +``` + +## Rich Error Details + +### Proto Definition +```protobuf +import "google/rpc/error_details.proto"; + +// Use with google.rpc.Status +``` + +### Server (Go) +```go +import ( + "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc/status" +) + +func (s *server) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.User, error) { + var violations []*errdetails.BadRequest_FieldViolation + + if req.Email == "" { + violations = append(violations, &errdetails.BadRequest_FieldViolation{ + Field: "email", + Description: "email is required", + }) + } + + if req.Name == "" { + violations = append(violations, &errdetails.BadRequest_FieldViolation{ + Field: "name", + Description: "name is required", + }) + } + + if len(violations) > 0 { + st := status.New(codes.InvalidArgument, "validation failed") + br := &errdetails.BadRequest{FieldViolations: violations} + st, _ = st.WithDetails(br) + return nil, st.Err() + } + + // Continue with creation... +} +``` + +### Client (Go) +```go +user, err := client.CreateUser(ctx, req) +if err != nil { + st, ok := status.FromError(err) + if !ok { + log.Fatalf("unknown error: %v", err) + } + + log.Printf("Error: %s (code: %s)", st.Message(), st.Code()) + + for _, detail := range st.Details() { + switch t := detail.(type) { + case *errdetails.BadRequest: + for _, violation := range t.GetFieldViolations() { + log.Printf(" - %s: %s", violation.Field, violation.Description) + } + case *errdetails.RetryInfo: + log.Printf(" Retry after: %v", t.RetryDelay.AsDuration()) + } + } +} +``` + +## Error Types for Different Scenarios + +```go +// User input errors → INVALID_ARGUMENT +status.Errorf(codes.InvalidArgument, "invalid email format") + +// Authentication → UNAUTHENTICATED +status.Errorf(codes.Unauthenticated, "invalid or expired token") + +// Authorization → PERMISSION_DENIED +status.Errorf(codes.PermissionDenied, "admin access required") + +// Resource not found → NOT_FOUND +status.Errorf(codes.NotFound, "user %s not found", id) + +// Conflict → ALREADY_EXISTS or ABORTED +status.Errorf(codes.AlreadyExists, "email already registered") + +// Rate limiting → RESOURCE_EXHAUSTED +status.Errorf(codes.ResourceExhausted, "rate limit exceeded") + +// Temporary failure → UNAVAILABLE +status.Errorf(codes.Unavailable, "database connection lost") + +// Bug → INTERNAL +status.Errorf(codes.Internal, "unexpected error: %v", err) +``` + +## Client Retry Logic + +```go +import "google.golang.org/grpc/codes" + +func isRetryable(code codes.Code) bool { + switch code { + case codes.Unavailable, codes.DeadlineExceeded, codes.Aborted: + return true + default: + return false + } +} +``` diff --git a/skills/grpc/references/performance.md b/skills/grpc/references/performance.md new file mode 100644 index 0000000..1707d9a --- /dev/null +++ b/skills/grpc/references/performance.md @@ -0,0 +1,204 @@ +# gRPC Performance Optimization + +## Connection Management + +### Connection Pooling + +```go +// gRPC manages connection pooling automatically +// But you can tune it + +conn, err := grpc.Dial(address, + grpc.WithDefaultServiceConfig(`{ + "loadBalancingPolicy": "round_robin", + "healthCheckConfig": { + "serviceName": "" + } + }`), +) +``` + +### Keepalive Settings + +```go +import "google.golang.org/grpc/keepalive" + +// Client +conn, err := grpc.Dial(address, + grpc.WithKeepaliveParams(keepalive.ClientParameters{ + Time: 10 * time.Second, // Ping interval + Timeout: 3 * time.Second, // Wait for pong + PermitWithoutStream: true, // Ping even without active streams + }), +) + +// Server +server := grpc.NewServer( + grpc.KeepaliveParams(keepalive.ServerParameters{ + MaxConnectionIdle: 15 * time.Minute, + MaxConnectionAge: 30 * time.Minute, + MaxConnectionAgeGrace: 5 * time.Second, + Time: 10 * time.Second, + Timeout: 3 * time.Second, + }), + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ + MinTime: 5 * time.Second, + PermitWithoutStream: true, + }), +) +``` + +## Message Size + +```go +// Default max: 4MB +// Increase if needed + +// Client +conn, err := grpc.Dial(address, + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(10*1024*1024), // 10MB + grpc.MaxCallSendMsgSize(10*1024*1024), + ), +) + +// Server +server := grpc.NewServer( + grpc.MaxRecvMsgSize(10*1024*1024), + grpc.MaxSendMsgSize(10*1024*1024), +) +``` + +## Compression + +```go +import "google.golang.org/grpc/encoding/gzip" + +// Client - per call +response, err := client.GetData(ctx, request, grpc.UseCompressor(gzip.Name)) + +// Client - default for all calls +conn, err := grpc.Dial(address, + grpc.WithDefaultCallOptions(grpc.UseCompressor(gzip.Name)), +) + +// Server - automatically handles compressed requests +// No special configuration needed +``` + +## Load Balancing + +### Client-Side Load Balancing + +```go +// DNS resolver with round-robin +conn, err := grpc.Dial( + "dns:///my-service.example.com:50051", + grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy": "round_robin"}`), +) +``` + +### Server-Side (with proxy) + +```yaml +# Envoy configuration example +listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 50051 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: AUTO + stat_prefix: grpc + route_config: + virtual_hosts: + - name: grpc_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: { cluster: grpc_backend } + http_filters: + - name: envoy.filters.http.router +``` + +## Streaming Performance + +### Buffer Settings + +```go +// Server - increase window size for high throughput +server := grpc.NewServer( + grpc.InitialWindowSize(1 << 20), // 1MB + grpc.InitialConnWindowSize(1 << 20), // 1MB +) + +// Client +conn, err := grpc.Dial(address, + grpc.WithInitialWindowSize(1 << 20), + grpc.WithInitialConnWindowSize(1 << 20), +) +``` + +### Batch Processing + +```go +// Instead of sending one message at a time +for _, item := range items { + stream.Send(&pb.Item{...}) // Slow! +} + +// Batch multiple items +batch := &pb.ItemBatch{Items: items} +stream.Send(batch) // Faster! +``` + +## Benchmarking + +```bash +# Using ghz +ghz --insecure \ + --proto ./user.proto \ + --call user.UserService.GetUser \ + -d '{"id": "123"}' \ + -c 100 \ # Concurrent workers + -n 10000 \ # Total requests + localhost:50051 + +# Output includes: +# - Requests/sec +# - Latency percentiles +# - Error rate +``` + +## Monitoring + +```go +import ( + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" +) + +// Server with OpenTelemetry +server := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), +) + +// Client +conn, err := grpc.Dial(address, + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), +) +``` + +## Best Practices Summary + +| Aspect | Recommendation | +|--------|----------------| +| **Connections** | Reuse connections, use keepalive | +| **Messages** | Keep small, use streaming for large data | +| **Compression** | Enable for large payloads | +| **Timeouts** | Always set deadlines | +| **Retries** | Implement with backoff | +| **Monitoring** | Use OpenTelemetry/Prometheus | diff --git a/skills/grpc/references/streaming.md b/skills/grpc/references/streaming.md new file mode 100644 index 0000000..aa68446 --- /dev/null +++ b/skills/grpc/references/streaming.md @@ -0,0 +1,219 @@ +# gRPC Streaming + +## Server Streaming + +Client sends one request, server sends multiple responses. + +### Proto Definition +```protobuf +service StockService { + rpc StreamPrices(StockRequest) returns (stream StockPrice); +} + +message StockRequest { + repeated string symbols = 1; +} + +message StockPrice { + string symbol = 1; + double price = 2; + google.protobuf.Timestamp timestamp = 3; +} +``` + +### Server (Go) +```go +func (s *server) StreamPrices(req *pb.StockRequest, stream pb.StockService_StreamPricesServer) error { + for { + for _, symbol := range req.Symbols { + price := getPrice(symbol) + if err := stream.Send(&pb.StockPrice{ + Symbol: symbol, + Price: price, + Timestamp: timestamppb.Now(), + }); err != nil { + return err + } + } + time.Sleep(time.Second) + } +} +``` + +### Client (Go) +```go +stream, err := client.StreamPrices(ctx, &pb.StockRequest{ + Symbols: []string{"AAPL", "GOOGL"}, +}) + +for { + price, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + fmt.Printf("%s: $%.2f\n", price.Symbol, price.Price) +} +``` + +## Client Streaming + +Client sends multiple messages, server responds once. + +### Proto Definition +```protobuf +service FileService { + rpc UploadFile(stream FileChunk) returns (UploadResponse); +} + +message FileChunk { + bytes content = 1; + string filename = 2; +} + +message UploadResponse { + string file_id = 1; + int64 bytes_received = 2; +} +``` + +### Server (Go) +```go +func (s *server) UploadFile(stream pb.FileService_UploadFileServer) error { + var totalBytes int64 + var filename string + + for { + chunk, err := stream.Recv() + if err == io.EOF { + return stream.SendAndClose(&pb.UploadResponse{ + FileId: generateID(), + BytesReceived: totalBytes, + }) + } + if err != nil { + return err + } + + filename = chunk.Filename + totalBytes += int64(len(chunk.Content)) + // Write chunk to storage... + } +} +``` + +### Client (Go) +```go +stream, err := client.UploadFile(ctx) + +file, _ := os.Open("large-file.bin") +buf := make([]byte, 1024*1024) // 1MB chunks + +for { + n, err := file.Read(buf) + if err == io.EOF { + break + } + + stream.Send(&pb.FileChunk{ + Content: buf[:n], + Filename: "large-file.bin", + }) +} + +response, err := stream.CloseAndRecv() +``` + +## Bidirectional Streaming + +Both client and server stream messages independently. + +### Proto Definition +```protobuf +service ChatService { + rpc Chat(stream ChatMessage) returns (stream ChatMessage); +} + +message ChatMessage { + string user = 1; + string content = 2; + google.protobuf.Timestamp timestamp = 3; +} +``` + +### Server (Go) +```go +func (s *server) Chat(stream pb.ChatService_ChatServer) error { + for { + msg, err := stream.Recv() + if err == io.EOF { + return nil + } + if err != nil { + return err + } + + // Broadcast to all connected clients + response := &pb.ChatMessage{ + User: "Server", + Content: fmt.Sprintf("Received: %s", msg.Content), + Timestamp: timestamppb.Now(), + } + + if err := stream.Send(response); err != nil { + return err + } + } +} +``` + +### Client (Go) +```go +stream, err := client.Chat(ctx) + +// Send in goroutine +go func() { + for _, msg := range messages { + stream.Send(&pb.ChatMessage{ + User: "Alice", + Content: msg, + }) + } + stream.CloseSend() +}() + +// Receive +for { + msg, err := stream.Recv() + if err == io.EOF { + break + } + fmt.Printf("[%s]: %s\n", msg.User, msg.Content) +} +``` + +## grpcurl with Streaming + +```bash +# Server streaming +grpcurl -plaintext -d '{"symbols": ["AAPL"]}' \ + localhost:50051 stock.StockService/StreamPrices + +# Client streaming (from stdin) +echo '{"content": "chunk1"} +{"content": "chunk2"}' | \ +grpcurl -plaintext -d @ localhost:50051 file.FileService/UploadFile + +# Bidirectional (interactive) +grpcurl -plaintext -d @ localhost:50051 chat.ChatService/Chat +``` + +## Best Practices + +1. **Handle EOF** - Always check for `io.EOF` in receive loops +2. **Graceful shutdown** - Use `CloseSend()` on client +3. **Backpressure** - Implement flow control for high-volume streams +4. **Keepalive** - Configure keepalive for long-lived streams +5. **Timeouts** - Use context deadlines appropriately diff --git a/skills/rest-api/SKILL.md b/skills/rest-api/SKILL.md new file mode 100644 index 0000000..946375b --- /dev/null +++ b/skills/rest-api/SKILL.md @@ -0,0 +1,149 @@ +--- +name: rest-api +version: 1.0.0 +description: REST API development and interaction skill. Use when designing RESTful APIs, making HTTP requests, debugging endpoints, or working with OpenAPI/Swagger specs. +argument-hint: "[GET|POST|PUT|DELETE] [endpoint] [--headers] [--data]" +--- + +# REST API Skill + +Design, build, and interact with RESTful APIs. + +## Quick Reference + +```bash +# GET request +curl -s https://api.example.com/users | python3 -m json.tool + +# GET with query params +curl -s "https://api.example.com/users?page=1&limit=10" + +# POST with JSON +curl -X POST https://api.example.com/users \ + -H "Content-Type: application/json" \ + -d '{"name": "John", "email": "john@example.com"}' + +# PUT update +curl -X PUT https://api.example.com/users/123 \ + -H "Content-Type: application/json" \ + -d '{"name": "John Updated"}' + +# DELETE +curl -X DELETE https://api.example.com/users/123 + +# With authentication +curl -H "Authorization: Bearer " https://api.example.com/me +``` + +## HTTP Methods + +| Method | Usage | Idempotent | Safe | +|--------|-------|------------|------| +| `GET` | Retrieve resource(s) | ✅ | ✅ | +| `POST` | Create resource | ❌ | ❌ | +| `PUT` | Replace resource | ✅ | ❌ | +| `PATCH` | Partial update | ❌ | ❌ | +| `DELETE` | Remove resource | ✅ | ❌ | +| `HEAD` | Get headers only | ✅ | ✅ | +| `OPTIONS` | Get allowed methods | ✅ | ✅ | + +## Status Codes + +| Range | Meaning | Common Codes | +|-------|---------|--------------| +| 2xx | Success | 200 OK, 201 Created, 204 No Content | +| 3xx | Redirect | 301 Moved, 304 Not Modified | +| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable | +| 5xx | Server Error | 500 Internal, 502 Bad Gateway, 503 Unavailable | + +## URL Design Best Practices + +``` +# Resources (nouns, plural) +GET /users # List users +GET /users/123 # Get user 123 +POST /users # Create user +PUT /users/123 # Replace user 123 +PATCH /users/123 # Update user 123 +DELETE /users/123 # Delete user 123 + +# Nested resources +GET /users/123/posts # User's posts +POST /users/123/posts # Create post for user + +# Filtering & pagination +GET /users?status=active&role=admin +GET /users?page=2&per_page=20 +GET /users?sort=created_at&order=desc + +# Actions (when CRUD doesn't fit) +POST /users/123/activate +POST /orders/456/cancel +``` + +## Request/Response Examples + +### Successful Response +```json +{ + "data": { + "id": "123", + "type": "user", + "attributes": { + "name": "John Doe", + "email": "john@example.com" + } + } +} +``` + +### Error Response +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Invalid input", + "details": [ + {"field": "email", "message": "Invalid email format"} + ] + } +} +``` + +### Pagination Response +```json +{ + "data": [...], + "meta": { + "total": 100, + "page": 1, + "per_page": 20, + "total_pages": 5 + }, + "links": { + "self": "/users?page=1", + "next": "/users?page=2", + "last": "/users?page=5" + } +} +``` + +## Headers + +| Header | Purpose | Example | +|--------|---------|---------| +| `Content-Type` | Request body format | `application/json` | +| `Accept` | Expected response format | `application/json` | +| `Authorization` | Auth credentials | `Bearer ` | +| `X-Request-ID` | Request tracing | `uuid` | +| `Cache-Control` | Caching behavior | `max-age=3600` | +| `ETag` | Resource version | `"abc123"` | + +## References + +| Topic | File | +|-------|------| +| OpenAPI/Swagger | [openapi.md](references/openapi.md) | +| Authentication | [auth.md](references/auth.md) | +| Versioning | [versioning.md](references/versioning.md) | +| Rate Limiting | [rate-limiting.md](references/rate-limiting.md) | diff --git a/skills/rest-api/references/auth.md b/skills/rest-api/references/auth.md new file mode 100644 index 0000000..8838b26 --- /dev/null +++ b/skills/rest-api/references/auth.md @@ -0,0 +1,113 @@ +# REST API Authentication + +## Authentication Methods + +### 1. Bearer Token (JWT) + +```bash +# Request +curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ + https://api.example.com/users + +# Login to get token +curl -X POST https://api.example.com/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email": "user@example.com", "password": "secret"}' + +# Response +{ + "access_token": "eyJhbGciOiJIUzI1NiIs...", + "refresh_token": "eyJhbGciOiJIUzI1NiIs...", + "expires_in": 3600 +} +``` + +### 2. API Key + +```bash +# Header +curl -H "X-API-Key: your-api-key" https://api.example.com/data + +# Query param (less secure) +curl "https://api.example.com/data?api_key=your-api-key" +``` + +### 3. Basic Auth + +```bash +curl -u username:password https://api.example.com/users + +# Or with header +curl -H "Authorization: Basic $(echo -n 'user:pass' | base64)" \ + https://api.example.com/users +``` + +### 4. OAuth 2.0 + +```bash +# Authorization Code Flow +# Step 1: Redirect user to authorize +https://auth.example.com/authorize? + response_type=code& + client_id=YOUR_CLIENT_ID& + redirect_uri=https://yourapp.com/callback& + scope=read+write& + state=random_state + +# Step 2: Exchange code for token +curl -X POST https://auth.example.com/token \ + -d "grant_type=authorization_code" \ + -d "code=AUTHORIZATION_CODE" \ + -d "client_id=YOUR_CLIENT_ID" \ + -d "client_secret=YOUR_SECRET" \ + -d "redirect_uri=https://yourapp.com/callback" + +# Step 3: Use access token +curl -H "Authorization: Bearer ACCESS_TOKEN" \ + https://api.example.com/me +``` + +## Token Refresh + +```bash +curl -X POST https://api.example.com/auth/refresh \ + -H "Content-Type: application/json" \ + -d '{"refresh_token": "your-refresh-token"}' +``` + +## Security Best Practices + +| Practice | Description | +|----------|-------------| +| **HTTPS only** | Never send credentials over HTTP | +| **Short-lived tokens** | Access tokens: 15min-1hr | +| **Secure refresh** | Refresh tokens: rotate on use | +| **Rate limiting** | Prevent brute force attacks | +| **Token revocation** | Implement logout/invalidation | +| **Scope limitation** | Request minimal permissions | + +## Error Responses + +```json +// 401 Unauthorized +{ + "error": "UNAUTHORIZED", + "message": "Invalid or expired token" +} + +// 403 Forbidden +{ + "error": "FORBIDDEN", + "message": "Insufficient permissions" +} +``` + +## CORS for Browser Clients + +```javascript +// Server-side headers +Access-Control-Allow-Origin: https://yourapp.com +Access-Control-Allow-Headers: Authorization, Content-Type +Access-Control-Allow-Methods: GET, POST, PUT, DELETE +Access-Control-Allow-Credentials: true +``` diff --git a/skills/rest-api/references/openapi.md b/skills/rest-api/references/openapi.md new file mode 100644 index 0000000..5a7579e --- /dev/null +++ b/skills/rest-api/references/openapi.md @@ -0,0 +1,200 @@ +# OpenAPI / Swagger Specification + +## Basic Structure (OpenAPI 3.0) + +```yaml +openapi: 3.0.3 +info: + title: My API + version: 1.0.0 + description: API description + +servers: + - url: https://api.example.com/v1 + description: Production + - url: https://staging-api.example.com/v1 + description: Staging + +paths: + /users: + get: + summary: List users + tags: [Users] + parameters: + - name: page + in: query + schema: + type: integer + default: 1 + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/UserList' + post: + summary: Create user + tags: [Users] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUserInput' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '400': + $ref: '#/components/responses/BadRequest' + + /users/{id}: + get: + summary: Get user by ID + tags: [Users] + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + $ref: '#/components/responses/NotFound' + +components: + schemas: + User: + type: object + properties: + id: + type: string + name: + type: string + email: + type: string + format: email + createdAt: + type: string + format: date-time + required: [id, name, email] + + CreateUserInput: + type: object + properties: + name: + type: string + minLength: 1 + maxLength: 100 + email: + type: string + format: email + required: [name, email] + + UserList: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/User' + meta: + $ref: '#/components/schemas/PaginationMeta' + + PaginationMeta: + type: object + properties: + total: + type: integer + page: + type: integer + perPage: + type: integer + + Error: + type: object + properties: + code: + type: string + message: + type: string + required: [code, message] + + responses: + BadRequest: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + NotFound: + description: Not Found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + +security: + - bearerAuth: [] +``` + +## Tools + +| Tool | Purpose | Command | +|------|---------|---------| +| **Swagger Editor** | Edit specs | `docker run -p 8080:8080 swaggerapi/swagger-editor` | +| **Swagger UI** | View docs | `docker run -p 8080:8080 -e SWAGGER_JSON=/spec.yaml swaggerapi/swagger-ui` | +| **openapi-generator** | Generate code | `openapi-generator generate -i spec.yaml -g typescript-axios -o ./client` | +| **Redoc** | Beautiful docs | `npx redoc-cli bundle spec.yaml` | + +## Validation + +```bash +# Validate spec +npx @apidevtools/swagger-cli validate openapi.yaml + +# Lint spec +npx @stoplight/spectral-cli lint openapi.yaml +``` + +## Generate from Code + +### Express + swagger-jsdoc +```javascript +const swaggerJSDoc = require('swagger-jsdoc'); + +const options = { + definition: { + openapi: '3.0.0', + info: { title: 'My API', version: '1.0.0' }, + }, + apis: ['./routes/*.js'], +}; + +const spec = swaggerJSDoc(options); +``` + +### FastAPI (Python) +```python +# Automatic OpenAPI generation +from fastapi import FastAPI +app = FastAPI() + +# Access at /openapi.json +``` diff --git a/skills/rest-api/references/rate-limiting.md b/skills/rest-api/references/rate-limiting.md new file mode 100644 index 0000000..9269620 --- /dev/null +++ b/skills/rest-api/references/rate-limiting.md @@ -0,0 +1,160 @@ +# REST API Rate Limiting + +## Rate Limit Headers + +```http +HTTP/1.1 200 OK +X-RateLimit-Limit: 1000 +X-RateLimit-Remaining: 999 +X-RateLimit-Reset: 1640995200 +Retry-After: 60 +``` + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Limit` | Max requests per window | +| `X-RateLimit-Remaining` | Requests left in window | +| `X-RateLimit-Reset` | Unix timestamp when window resets | +| `Retry-After` | Seconds until retry (on 429) | + +## 429 Too Many Requests + +```bash +HTTP/1.1 429 Too Many Requests +Content-Type: application/json +Retry-After: 60 + +{ + "error": "RATE_LIMIT_EXCEEDED", + "message": "Too many requests. Try again in 60 seconds.", + "retryAfter": 60 +} +``` + +## Common Rate Limit Strategies + +### 1. Fixed Window + +``` +100 requests per minute +Window: 00:00 - 00:59 +``` + +**Pros:** Simple +**Cons:** Burst at window edges + +### 2. Sliding Window + +``` +100 requests per rolling 60 seconds +``` + +**Pros:** Smoother distribution +**Cons:** More complex to implement + +### 3. Token Bucket + +``` +Bucket size: 100 tokens +Refill rate: 10 tokens/second +``` + +**Pros:** Allows controlled bursts +**Cons:** More complex + +## Client-Side Handling + +```javascript +async function fetchWithRetry(url, options = {}, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + const response = await fetch(url, options); + + if (response.status === 429) { + const retryAfter = response.headers.get('Retry-After') || 60; + console.log(`Rate limited. Retrying in ${retryAfter}s...`); + await sleep(retryAfter * 1000); + continue; + } + + return response; + } + throw new Error('Max retries exceeded'); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} +``` + +## Exponential Backoff + +```javascript +async function exponentialBackoff(fn, maxRetries = 5) { + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + if (error.status !== 429 || attempt === maxRetries - 1) { + throw error; + } + + const delay = Math.min(1000 * Math.pow(2, attempt), 32000); + const jitter = Math.random() * 1000; + await sleep(delay + jitter); + } + } +} +``` + +## Server-Side Implementation + +### Express (express-rate-limit) + +```javascript +const rateLimit = require('express-rate-limit'); + +const limiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 100, + standardHeaders: true, + legacyHeaders: false, + message: { + error: 'RATE_LIMIT_EXCEEDED', + message: 'Too many requests' + } +}); + +app.use('/api/', limiter); +``` + +### Redis-based (for distributed systems) + +```javascript +const Redis = require('ioredis'); +const redis = new Redis(); + +async function checkRateLimit(key, limit, windowSec) { + const current = await redis.incr(key); + + if (current === 1) { + await redis.expire(key, windowSec); + } + + return { + allowed: current <= limit, + remaining: Math.max(0, limit - current), + reset: await redis.ttl(key) + }; +} +``` + +## Best Practices + +| Practice | Description | +|----------|-------------| +| **Return headers** | Always include rate limit info | +| **Document limits** | State limits in API docs | +| **Graceful 429** | Provide retry-after info | +| **Different tiers** | Higher limits for paid plans | +| **Key by user** | Not just IP (for auth'd APIs) | +| **Separate limits** | Per endpoint if needed | diff --git a/skills/rest-api/references/versioning.md b/skills/rest-api/references/versioning.md new file mode 100644 index 0000000..efe4215 --- /dev/null +++ b/skills/rest-api/references/versioning.md @@ -0,0 +1,137 @@ +# REST API Versioning + +## Versioning Strategies + +### 1. URL Path Versioning (Recommended) + +``` +GET /v1/users +GET /v2/users +``` + +**Pros:** Clear, cacheable, easy routing +**Cons:** URL pollution + +```javascript +// Express routing +const v1Router = require('./routes/v1'); +const v2Router = require('./routes/v2'); + +app.use('/v1', v1Router); +app.use('/v2', v2Router); +``` + +### 2. Header Versioning + +```bash +curl -H "Accept: application/vnd.api+json; version=2" \ + https://api.example.com/users + +# Or custom header +curl -H "API-Version: 2" https://api.example.com/users +``` + +**Pros:** Clean URLs +**Cons:** Hidden, harder to test + +### 3. Query Parameter + +``` +GET /users?version=2 +``` + +**Pros:** Easy to test +**Cons:** Awkward, pollutes query params + +### 4. Content Negotiation (Media Type) + +```bash +curl -H "Accept: application/vnd.company.v2+json" \ + https://api.example.com/users +``` + +**Pros:** RESTful, flexible +**Cons:** Complex, less discoverable + +## Version Lifecycle + +| Stage | Description | Duration | +|-------|-------------|----------| +| **Current** | Active development | Ongoing | +| **Supported** | Bug fixes only | 6-12 months | +| **Deprecated** | Warnings, no fixes | 3-6 months | +| **Sunset** | Removed | - | + +## Deprecation Headers + +```http +HTTP/1.1 200 OK +Deprecation: Sun, 01 Jan 2025 00:00:00 GMT +Sunset: Sun, 01 Jul 2025 00:00:00 GMT +Link: ; rel="successor-version" +``` + +## Backward Compatibility Tips + +### Adding Fields (Safe) +```json +// v1 +{ "name": "John" } + +// v2 - adds email (backward compatible) +{ "name": "John", "email": "john@example.com" } +``` + +### Changing Field Names (Breaking) +```json +// Instead of renaming, add alias +{ + "name": "John", // Keep old + "fullName": "John Doe" // Add new +} +``` + +### Removing Fields +```json +// 1. Deprecate first (return null, add warning) +{ "oldField": null, "newField": "value" } + +// 2. Remove in next major version +{ "newField": "value" } +``` + +## Migration Guide Template + +```markdown +# Migrating from v1 to v2 + +## Breaking Changes +- `user.name` renamed to `user.fullName` +- Removed `user.legacyId` field + +## New Features +- Added `user.avatar` field +- New `/users/search` endpoint + +## Deprecations +- `/users/find` deprecated, use `/users/search` + +## Migration Steps +1. Update client to use new field names +2. Test with v2 endpoint +3. Remove v1 references +``` + +## Semantic Versioning for APIs + +``` +v{MAJOR}.{MINOR} + +MAJOR: Breaking changes +MINOR: Backward-compatible additions + +Examples: +- v1 → v2: Breaking change +- v1.1: New optional field +- v1.2: New endpoint +```