| name | api-architecture |
| description | Enterprise API design with REST, GraphQL, gRPC patterns including versioning, pagination, and error handling |
| category | backend |
| triggers | ["api architecture","api design","rest api","graphql","grpc","api versioning","pagination"] |
API Architecture
Enterprise-grade API design patterns following BigTech standards. This skill covers REST, GraphQL, and gRPC design with versioning, pagination, rate limiting, and comprehensive error handling.
Purpose
Design APIs that scale and delight developers:
- Apply REST best practices consistently
- Implement GraphQL for flexible queries
- Design gRPC for high-performance services
- Handle versioning without breaking clients
- Implement robust pagination patterns
- Create comprehensive error responses
Features
1. RESTful API Design
import express from 'express';
import { z } from 'zod';
const router = express.Router();
const ListUsersSchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
sort: z.enum(['created_at', 'name', 'email']).default('created_at'),
order: z.enum(['asc', 'desc']).default('desc'),
status: z.enum(['active', 'inactive', 'all']).optional(),
});
router.get('/users', async (req, res) => {
const query = ListUsersSchema.parse(req.query);
const { users, total } = await userService.list(query);
res.json({
data: users,
pagination: {
page: query.page,
limit: query.limit,
total,
totalPages: Math.ceil(total / query.limit),
hasMore: query.page * query.limit < total,
},
links: {
self: `/api/v1/users?page=${query.page}&limit=${query.limit}`,
first: `/api/v1/users?page=1&limit=${query.limit}`,
last: `/api/v1/users?page=${Math.ceil(total / query.limit)}&limit=${query.limit}`,
next: query.page * query.limit < total
? `/api/v1/users?page=${query.page + 1}&limit=${query.limit}`
: null,
prev: query.page > 1
? `/api/v1/users?page=${query.page - 1}&limit=${query.limit}`
: null,
},
});
});
router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: {
code: 'USER_NOT_FOUND',
message: 'User not found',
details: { id: req.params.id },
},
});
}
res.json({ data: user });
});
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2).max(100),
password: z.string().min(8),
role: z.enum(['user', 'admin']).default('user'),
});
router.post('/users', async (req, res) => {
const data = CreateUserSchema.parse(req.body);
const user = await userService.create(data);
res.status(201)
.location(`/api/v1/users/${user.id}`)
.json({ data: user });
});
const UpdateUserSchema = CreateUserSchema.partial().omit({ password: true });
router.patch('/users/:id', async (req, res) => {
const data = UpdateUserSchema.parse(req.body);
const user = await userService.update(req.params.id, data);
if (!user) {
return res.status(404).json({
error: { code: 'USER_NOT_FOUND', message: 'User not found' },
});
}
res.json({ data: user });
});
router.delete('/users/:id', async (req, res) => {
const deleted = await userService.delete(req.params.id);
if (!deleted) {
return res.status(404).json({
error: { code: 'USER_NOT_FOUND', message: 'User not found' },
});
}
res.status(204).send();
});
2. Error Handling Standards
interface APIError {
code: string;
message: string;
details?: unknown;
requestId?: string;
documentation?: string;
}
const ERROR_STATUS_MAP: Record<string, number> = {
VALIDATION_ERROR: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
CONFLICT: 409,
RATE_LIMITED: 429,
INTERNAL_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
class APIException extends Error {
constructor(
public code: string,
message: string,
?: ,
: = ERROR_STATUS_MAP[code] ||
) {
(message);
. = ;
}
(): {
{
: .,
: .,
: .,
};
}
}
{
() {
(
,
,
errors..( ({
: e..(),
: e.,
: e.,
})),
);
}
}
{
() {
(
,
,
{ resource, id },
);
}
}
() {
requestId = req.[] ;
logger.({
requestId,
: err.,
: err.,
: req.,
: req.,
});
(err ) {
res.(err.).({
: {
...err.(),
requestId,
},
});
}
(err z.) {
res.().({
: (err).(),
});
}
res.().({
: {
: ,
: ,
requestId,
},
});
}
3. API Versioning
const v1Router = express.Router();
const v2Router = express.Router();
v1Router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
res.json(user);
});
v2Router.get('/users/:id', async (req, res) => {
const user = await userService.findById(req.params.id);
res.json({
data: user,
meta: { version: 'v2' },
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
function versionMiddleware(req: Request, res: Response, next: NextFunction) {
const version = req.headers[] || req.[] || ;
req. = version;
();
}
app.(, {
user = userService.(req..);
(req. === ) {
res.({ : user });
}
res.(user);
});
router.(, {
res.(, );
res.(, );
res.(, );
();
});
4. Rate Limiting
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
const basicLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true,
legacyHeaders: false,
store: new RedisStore({
sendCommand: (...args: string[]) => redis.call(...args),
}),
handler: (req, res) => {
res.status(429).json({
error: {
code: 'RATE_LIMITED',
message: 'Too many requests',
retryAfter: res.getHeader(),
},
});
},
});
() {
limits = {
: { : , : },
: { : , : },
: { : , : },
};
({
...limits[tier],
: ,
: ({ : redis.(...args) }),
});
}
strictLimiter = ({
: * ,
: ,
: { : { : , : } },
});
router.(, strictLimiter, loginHandler);
(): <{ : ; : ; : }> {
now = .();
windowStart = now - windowSeconds * ;
multi = redis.();
multi.(key, , windowStart);
multi.(key, now.(), );
multi.(key);
multi.(key, windowSeconds);
results = multi.();
count = results?.[]?.[] ;
{
: count <= limit,
: .(, limit - count),
: .((windowStart + windowSeconds * ) / ),
};
}
5. GraphQL Schema Design
import { makeExecutableSchema } from '@graphql-tools/schema';
const typeDefs = `#graphql
type Query {
user(id: ID!): User
users(
first: Int
after: String
filter: UserFilter
orderBy: UserOrderBy
): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
deleteUser(id: ID!): DeleteUserPayload!
}
# Relay-style pagination
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
cursor: String!
node: User!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type User {
id: ID!
email: String!
name: String!
status: UserStatus!
createdAt: DateTime!
updatedAt: DateTime!
posts(first: Int, after: String): PostConnection!
}
enum UserStatus {
ACTIVE
INACTIVE
SUSPENDED
}
input UserFilter {
status: UserStatus
search: String
createdAfter: DateTime
createdBefore: DateTime
}
input UserOrderBy {
field: UserOrderField!
direction: OrderDirection!
}
enum UserOrderField {
CREATED_AT
NAME
EMAIL
}
enum OrderDirection {
ASC
DESC
}
# Input types for mutations
input CreateUserInput {
email: String!
name: String!
password: String!
}
# Payload types for mutations
type CreateUserPayload {
user: User
errors: [UserError!]
}
type UserError {
field: String!
message: String!
code: String!
}
scalar DateTime
`;
const resolvers = {
Query: {
user: async (_, { id }, ctx) => {
return ctx.loaders.user.load(id);
},
users: async (_, args, ctx) => {
const { first = 20, after, filter, orderBy } = args;
const { users, total, hasMore } = await userService.list({
limit: first,
cursor: after ? decodeCursor(after) : ,
filter,
orderBy,
});
edges = users.( ({
: (user.),
: user,
}));
{
edges,
: total,
: {
: hasMore,
: !!after,
: edges[]?.,
: edges[edges. - ]?.,
},
};
},
},
: {
: (_, { input }, ctx) => {
{
user = userService.(input);
{ user, : [] };
} (error) {
{
: ,
: [{ : , : error., : }],
};
}
},
},
: {
: (user, args, ctx) => {
ctx...({ : user., ...args });
},
},
};
;
() {
{
: ( (: []) => {
users = userService.(ids);
ids.( users.( u. === id));
}),
: ( (keys) => {
userIds = keys.( k.);
posts = postService.(userIds);
keys.(
posts.( p. === key.)
);
}),
};
}
6. OpenAPI Specification
openapi: 3.1.0
info:
title: User API
version: 1.0.0
description: User management API
contact:
email: api@example.com
license:
name: MIT
servers:
- url: https://api.example.com/v1
description: Production
- url: https://staging-api.example.com/v1
description: Staging
paths:
/users:
get:
summary: List users
operationId: listUsers
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: limit
[]
[, , , , ]
[, , ]
[, , ]
[, ]
[]
Use Cases
1. Public API Design
router.get('/products', async (req, res) => {
const requestId = req.headers['x-request-id'] || generateRequestId();
res.set('X-Request-ID', requestId);
res.set('X-RateLimit-Limit', '1000');
res.set('X-RateLimit-Remaining', String(remaining));
res.set('X-RateLimit-Reset', String(resetTime));
res.json({
data: products,
pagination: { ... },
meta: {
requestId,
apiVersion: 'v1',
},
});
});
2. Internal Microservice API
syntax = "proto3";
package user;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (User);
}
message User {
string id = 1;
string email = 2;
string name = 3;
UserStatus status = 4;
}
enum UserStatus {
UNKNOWN = 0;
ACTIVE = 1;
INACTIVE = 2;
}
Best Practices
Do's
- Use consistent naming - Plural nouns for collections
- Return appropriate status codes - 201 for create, 204 for delete
- Include request IDs - For debugging and support
- Document everything - OpenAPI/Swagger specs
- Version from day one - Avoid breaking changes
- Implement idempotency - For POST/PUT operations
Don'ts
- Don't use verbs in URLs
- Don't return 200 for errors
- Don't expose internal errors
- Don't skip pagination
- Don't ignore cache headers
- Don't forget rate limiting
Related Skills
- backend-development - Implementation patterns
- security - API security
- caching-strategies - Response caching
Reference Resources