Skip to main content
/ Web Dev

API Development Best Practices: Building Integrations That Scale

Sacha Roussakis-NotterSacha Roussakis-Notter
16 min read
TypeScript
Node.js
Share

Learn API development fundamentals and best practices for building scalable integrations. Covers REST, GraphQL, security, and documentation strategies.

Why API Design Matters

APIs are the backbone of modern software. They connect mobile apps to backends, enable third-party integrations, power microservices architectures, and expose business capabilities to partners and customers.

A well-designed API:

  • Reduces integration time from weeks to hours
  • Minimises support burden through clear, predictable behaviour
  • Scales gracefully under increasing load
  • Enables ecosystem growth and partner adoption

A poorly designed API:

  • Creates friction for every consumer
  • Generates endless support tickets
  • Becomes a bottleneck as usage grows
  • Accumulates technical debt that compounds over time

This guide covers the essential patterns, security practices, and architectural decisions for building APIs that scale.

REST vs GraphQL: Choosing the Right Approach

When to Use REST

REST (Representational State Transfer) remains the dominant API architecture for good reason: simplicity, familiarity, and tooling maturity.

flowchart

Client

REST API

GET /users

POST /orders

PUT /users/123

DELETE /products/456

Ctrl+scroll to zoom • Drag to pan62%

REST is ideal for:

Use CaseWhy REST Works
CRUD operationsNatural mapping to HTTP methods
Public APIsWidely understood, excellent tooling
Simple data structuresStraightforward resource representation
Caching requirementsHTTP caching works natively
Broad client compatibilityEvery language/platform supports REST

REST Example: User Endpoint

typescript
1// GET /api/users/123
2{
3 "id": 123,
4 "name": "John Smith",
5 "email": "john@example.com",
6 "created_at": "2026-01-06T10:30:00Z",
7 "links": {
8 "self": "/api/users/123",
9 "orders": "/api/users/123/orders",
10 "profile": "/api/users/123/profile"
11 }
12}

When to Use GraphQL

GraphQL excels when clients need flexibility in data fetching—particularly mobile apps and complex frontends.

flowchart

Client

GraphQL API

Single Endpoint

Query Resolver

Fetch Exact Data Needed

Ctrl+scroll to zoom • Drag to pan48%

GraphQL is ideal for:

Use CaseWhy GraphQL Works
Mobile applicationsMinimise data transfer, avoid over-fetching
Complex, nested dataSingle request for related entities
Rapidly evolving frontendsClients define their data needs
Multiple client typesDifferent views need different data shapes
Real-time subscriptionsBuilt-in subscription support

GraphQL Example: Flexible Query

graphql
1# Client requests exactly what it needs
2query GetUserWithOrders {
3 user(id: 123) {
4 name
5 email
6 orders(last: 5) {
7 id
8 total
9 status
10 items {
11 product {
12 name
13 price
14 }
15 }
16 }
17 }
18}

Comparison Matrix

FactorRESTGraphQL
Learning curveLowMedium
Tooling maturityExcellentGood
CachingNative HTTP cachingRequires custom setup
Over-fetchingCommon problemSolved by design
Under-fetchingMultiple requests neededSingle request
VersioningURL or header versioningSchema evolution
Error handlingHTTP status codesCustom error format
File uploadsStraightforwardMore complex

Core API Design Principles

1. Consistent Resource Naming

plaintext
1# Good - Nouns, plural, lowercase, hyphens
2GET /api/users
3GET /api/users/123
4GET /api/users/123/orders
5GET /api/order-items
6
7# Bad - Verbs, inconsistent, underscores
8GET /api/getUser/123
9GET /api/Users
10GET /api/user_orders
11POST /api/createOrder

2. Use HTTP Methods Correctly

MethodPurposeIdempotentSafe
GETRetrieve resourceYesYes
POSTCreate resourceNoNo
PUTReplace resource entirelyYesNo
PATCHPartial updateYesNo
DELETERemove resourceYesNo

3. Meaningful HTTP Status Codes

typescript
1// Success codes
2200 OK // Standard success
3201 Created // Resource created (POST)
4204 No Content // Success with no response body (DELETE)
5
6// Client errors
7400 Bad Request // Invalid request syntax/parameters
8401 Unauthorized // Authentication required
9403 Forbidden // Authenticated but not authorised
10404 Not Found // Resource doesn't exist
11409 Conflict // Resource conflict (duplicate, version mismatch)
12422 Unprocessable Entity // Validation errors
13429 Too Many Requests // Rate limit exceeded
14
15// Server errors
16500 Internal Server Error // Unexpected server error
17502 Bad Gateway // Upstream service error
18503 Service Unavailable // Temporary overload/maintenance

4. Structured Error Responses

typescript
1// Consistent error format
2interface ApiError {
3 error: {
4 code: string; // Machine-readable error code
5 message: string; // Human-readable description
6 details?: { // Field-level errors for validation
7 field: string;
8 message: string;
9 }[];
10 request_id: string; // For debugging/support
11 };
12}
13
14// Example response
15{
16 "error": {
17 "code": "VALIDATION_ERROR",
18 "message": "Request validation failed",
19 "details": [
20 { "field": "email", "message": "Invalid email format" },
21 { "field": "age", "message": "Must be 18 or older" }
22 ],
23 "request_id": "req_abc123xyz"
24 }
25}

Pagination, Filtering, and Sorting

Pagination Patterns

Offset-based (simple, but has issues at scale):

plaintext
1GET /api/users?limit=20&offset=40

Cursor-based (better for large datasets):

plaintext
1GET /api/users?limit=20&cursor=eyJpZCI6MTAwfQ

Implementation Example:

typescript
1// Response with pagination metadata
2{
3 "data": [...],
4 "pagination": {
5 "total": 1250,
6 "limit": 20,
7 "has_more": true,
8 "next_cursor": "eyJpZCI6MTIwfQ",
9 "prev_cursor": "eyJpZCI6MTAwfQ"
10 }
11}

Filtering and Sorting

plaintext
1# Filtering
2GET /api/orders?status=pending&customer_id=123
3
4# Sorting
5GET /api/products?sort=price&order=asc
6
7# Combined
8GET /api/orders?status=shipped&sort=created_at&order=desc&limit=50

Authentication and Authorisation

Authentication Methods

MethodUse CaseProsCons
API KeysServer-to-server, simple integrationsEasy to implementNo user context, limited security
OAuth 2.0Third-party access, user delegationIndustry standard, secureComplex implementation
JWTStateless auth, microservicesScalable, self-containedToken size, no revocation
Session-basedWeb applicationsSimple, revocableRequires server state

JWT Implementation

typescript
1// JWT structure
2interface JwtPayload {
3 sub: string; // Subject (user ID)
4 iat: number; // Issued at
5 exp: number; // Expiration
6 iss: string; // Issuer
7 aud: string; // Audience
8 scope: string[]; // Permissions
9}
10
11// Example payload
12{
13 "sub": "user_123",
14 "iat": 1704528000,
15 "exp": 1704531600,
16 "iss": "api.yourcompany.com",
17 "aud": "yourcompany.com",
18 "scope": ["read:users", "write:orders"]
19}

Authorisation Patterns

flowchart

No

Yes

No

Yes

No

Yes

Request

Authenticated?

401 Unauthorized

Has Permission?

403 Forbidden

Owns Resource?

Process Request

Ctrl+scroll to zoom • Drag to pan31%

Role-Based Access Control (RBAC):

typescript
1// Permission check middleware
2const requirePermission = (permission: string) => {
3 return (req: Request, res: Response, next: NextFunction) => {
4 const userPermissions = req.user?.permissions || [];
5
6 if (!userPermissions.includes(permission)) {
7 return res.status(403).json({
8 error: {
9 code: "FORBIDDEN",
10 message: "Insufficient permissions",
11 required: permission
12 }
13 });
14 }
15
16 next();
17 };
18};
19
20// Usage
21app.delete(
22 "/api/users/:id",
23 authenticate,
24 requirePermission("users:delete"),
25 deleteUser
26);

Rate Limiting

Rate limiting protects your API from abuse and ensures fair resource allocation.

Rate Limiting Algorithms

AlgorithmDescriptionBest For
Fixed WindowCount requests per time windowSimple implementation
Sliding WindowRolling window of requestsSmoother limits
Token BucketTokens replenish over timeHandling bursts
Leaky BucketConstant drain rateConsistent throughput

Implementation Example

typescript
1// Rate limit configuration
2interface RateLimitConfig {
3 window_ms: number; // Time window in milliseconds
4 max_requests: number; // Maximum requests per window
5 key_generator: (req: Request) => string; // Identifier
6}
7
8// Different limits for different endpoints
9const rateLimits = {
10 default: { window_ms: 60000, max_requests: 100 },
11 auth: { window_ms: 900000, max_requests: 5 }, // 5 per 15 min
12 search: { window_ms: 60000, max_requests: 30 },
13 export: { window_ms: 3600000, max_requests: 10 }, // 10 per hour
14};
15
16// Response headers
17X-RateLimit-Limit: 100
18X-RateLimit-Remaining: 87
19X-RateLimit-Reset: 1704528060

Real-World Examples

ServiceLimitNotes
GitHub API5,000/hour (authenticated)60/hour unauthenticated
Stripe API100/secondHigher for production
Twitter API300/15min (tweets)Varies by endpoint
OpenAI APITokens per minuteBased on model and tier

API Security Best Practices

OWASP API Security Top 10

RiskDescriptionMitigation
Broken Object Level AuthAccessing others' resourcesVerify ownership on every request
Broken AuthenticationWeak auth mechanismsStrong passwords, MFA, secure tokens
Broken Object Property AuthMass assignment attacksExplicit allowlists for writable fields
Unrestricted Resource ConsumptionDoS via resource exhaustionRate limiting, pagination limits
Broken Function Level AuthAccessing admin functionsRole-based access control
Unrestricted Access to Sensitive FlowsAutomated attacks on business flowsCAPTCHA, anomaly detection
Server-Side Request ForgeryInternal network accessURL validation, network segmentation
Security MisconfigurationDefault configs, verbose errorsSecurity hardening, minimal info
Improper Inventory ManagementShadow APIs, old versionsAPI catalogue, version sunset policies
Unsafe Consumption of APIsTrusting third-party dataValidate all external input

Security Implementation Checklist

  • HTTPS only—never accept HTTP
  • Input validation—validate all parameters
  • Output encoding—prevent injection in responses
  • Authentication on all endpoints—no exceptions
  • Authorisation checks—verify permissions
  • Rate limiting—prevent abuse
  • Request size limits—prevent resource exhaustion
  • Logging and monitoring—detect anomalies
  • CORS configuration—restrict origins appropriately
  • Security headers—CSP, HSTS, X-Content-Type-Options

Input Validation Example

typescript
1import { z } from "zod";
2
3// Define schema
4const CreateUserSchema = z.object({
5 email: z.string().email().max(255),
6 name: z.string().min(1).max(100),
7 age: z.number().int().min(18).max(150).optional(),
8 role: z.enum(["user", "admin"]).default("user"),
9});
10
11// Validation middleware
12const validate = (schema: z.ZodSchema) => {
13 return (req: Request, res: Response, next: NextFunction) => {
14 try {
15 req.body = schema.parse(req.body);
16 next();
17 } catch (error) {
18 if (error instanceof z.ZodError) {
19 return res.status(422).json({
20 error: {
21 code: "VALIDATION_ERROR",
22 message: "Invalid request data",
23 details: error.errors.map(e => ({
24 field: e.path.join("."),
25 message: e.message
26 }))
27 }
28 });
29 }
30 next(error);
31 }
32 };
33};

GraphQL-Specific Security

GraphQL introduces unique security considerations:

Query Complexity Limiting

typescript
1// Prevent expensive queries
2const complexityLimit = 1000;
3
4const complexityRules = {
5 User: {
6 complexity: 1,
7 orders: { complexity: 10, multiplier: "first" }
8 },
9 Order: {
10 complexity: 5,
11 items: { complexity: 2, multiplier: "first" }
12 }
13};
14
15// Reject queries exceeding limit
16query TooComplex {
17 users(first: 100) { # 100 * 1 = 100
18 orders(first: 50) { # 100 * 50 * 10 = 50,000 <- Rejected!
19 items(first: 10) {
20 product { name }
21 }
22 }
23 }
24}

Depth Limiting

typescript
1// Prevent deeply nested queries
2const maxDepth = 5;
3
4// This query would be rejected (depth = 6)
5query TooDeep {
6 user {
7 orders {
8 items {
9 product {
10 category {
11 parent { # Depth 6 - rejected
12 name
13 }
14 }
15 }
16 }
17 }
18 }
19}

Batch Attack Prevention

typescript
1// Limit aliased queries
2const maxAliases = 10;
3
4// Attacker attempts batching
5query BatchAttack {
6 u1: user(id: 1) { password } # Alias 1
7 u2: user(id: 2) { password } # Alias 2
8 # ... 10,000 more aliases
9}

API Versioning Strategies

URL Versioning

plaintext
1GET /api/v1/users
2GET /api/v2/users

Pros: Clear, cacheable, easy routing

Cons: URL pollution, harder deprecation

Header Versioning

plaintext
1GET /api/users
2Accept: application/vnd.api+json;version=2

Pros: Clean URLs, flexible

Cons: Less visible, harder to test

Query Parameter Versioning

plaintext
1GET /api/users?version=2

Pros: Easy to implement

Cons: Not cacheable, feels hacky

Version ChangeWhen to Use
Patch (v1.0.1)Bug fixes, no API changes
Minor (v1.1.0)Backward-compatible additions
Major (v2.0.0)Breaking changes

API Documentation

Documentation Must-Haves

ElementPurpose
Quick start guideGet users to first API call fast
AuthenticationHow to obtain and use credentials
Endpoint referenceEvery endpoint, parameter, response
Code examplesMultiple languages, copy-paste ready
Error referenceAll error codes and meanings
Rate limitsLimits and how to handle them
ChangelogWhat changed between versions
SDKsOfficial libraries if available

OpenAPI Specification Example

yaml
1openapi: 3.0.0
2info:
3 title: User API
4 version: 1.0.0
5paths:
6 /users/{id}:
7 get:
8 summary: Get user by ID
9 parameters:
10 - name: id
11 in: path
12 required: true
13 schema:
14 type: integer
15 responses:
16 '200':
17 description: User found
18 content:
19 application/json:
20 schema:
21 $ref: '#/components/schemas/User'
22 '404':
23 description: User not found
24components:
25 schemas:
26 User:
27 type: object
28 properties:
29 id:
30 type: integer
31 email:
32 type: string
33 format: email
34 name:
35 type: string

Performance and Scalability

Caching Strategies

typescript
1// HTTP caching headers
2Cache-Control: public, max-age=3600 // Cache 1 hour
3Cache-Control: private, max-age=600 // Private cache 10 min
4Cache-Control: no-store // Never cache
5ETag: "abc123" // Conditional requests
6
7// Conditional request flow
8// 1. Client sends: If-None-Match: "abc123"
9// 2. Server checks: ETag matches?
10// 3. If match: 304 Not Modified (no body)
11// 4. If changed: 200 OK with new data and ETag

Database Query Optimisation

TechniqueWhen to Use
Select specific columnsAlways—avoid SELECT *
Index filtered columnsFrequently queried fields
Connection poolingHigh-traffic APIs
Query result cachingExpensive, repeated queries
Read replicasRead-heavy workloads

Async Processing for Long Operations

flowchart

No

Yes

Client

API

Queue Job

Return 202 Accepted

Job ID + Status URL

Background Worker

Process Job

Update Status

Client Polls

GET /jobs/123

Complete?

Return Progress

Return Result

Ctrl+scroll to zoom • Drag to pan43%
typescript
1// Long-running operation pattern
2// POST /api/reports
3// Response: 202 Accepted
4{
5 "job_id": "job_xyz123",
6 "status": "queued",
7 "status_url": "/api/jobs/job_xyz123",
8 "estimated_completion": "2026-01-06T12:00:00Z"
9}
10
11// GET /api/jobs/job_xyz123
12{
13 "job_id": "job_xyz123",
14 "status": "completed",
15 "result_url": "/api/reports/rpt_abc789",
16 "completed_at": "2026-01-06T11:55:00Z"
17}

Monitoring and Observability

Key Metrics to Track

MetricTargetAction Threshold
Response time (p50)< 100ms> 200ms
Response time (p99)< 500ms> 1s
Error rate< 0.1%> 1%
Availability> 99.9%< 99.5%
Requests per secondMonitor trendSudden spikes

Structured Logging

typescript
1// Every request should log
2{
3 "timestamp": "2026-01-06T10:30:00Z",
4 "request_id": "req_abc123",
5 "method": "POST",
6 "path": "/api/orders",
7 "status": 201,
8 "duration_ms": 45,
9 "user_id": "user_123",
10 "ip": "203.0.113.50",
11 "user_agent": "MyApp/1.0"
12}

About Buun Group

At Buun Group, we design and build APIs that power business growth. Our approach:

  • API-first design: APIs as products, not afterthoughts
  • Security by default: Authentication, authorisation, rate limiting built in
  • Documentation: OpenAPI specs, SDKs, and developer guides
  • Scalability: Architectures that handle growth

Whether you're building a SaaS platform, mobile app backend, or partner integration layer, we can help you design APIs that developers love to use.

Need help designing your API?

Topics

API development servicescustom software developmentSaaS development servicesREST API designGraphQL developmentAPI securityAPI integrationBrisbane API development

Share this post

Share

Comments

Sign in to join the conversation

Login

No comments yet. Be the first to share your thoughts!

Found an issue with this article?

/ Let's Talk

Want to work with us?

Whether you need help with architecture, development, or technical consulting, our team is here to help bring your vision to life.