What is an API? The Contract That Makes Software Work Together

An API is a contract between two pieces of software. One side defines what requests it accepts and what responses it sends. The other side follows that contract to get work done. Every app on your phone, every website pulling live data, and every service talking to another service uses APIs.

Every app on your phone communicates with a server through an API. The weather app fetching current conditions. The banking app showing your balance. The ride-hailing app tracking the car. The social feed loading posts. None of these apps store data on your phone. They send requests to servers and receive responses. The mechanism for those requests and responses is an API.

API stands for Application Programming Interface. That definition sounds circular until you break it down: it is an interface between two applications that defines how one program can use the capabilities of another. An interface specifies a contract: here is what you can ask for, here is how to ask for it, and here is what you will receive in return.

APIs exist at every layer of software. The operating system has APIs that let applications read files, make network connections, and draw to the screen. Programming languages have standard library APIs. Databases have query APIs. Hardware has driver APIs. When developers talk about APIs in the context of web development, they usually mean HTTP-based APIs that let one service communicate with another over the network.

This article covers what APIs are, how they are designed, what the different styles mean, and what production API design actually looks like.

The Problem APIs Solve

Before APIs existed as a formal concept, integrating software meant tight coupling. To use data from another system, you either copied the data manually, shared a database directly, or wrote code that reached into another system's internals. All three approaches created fragile dependencies: changes to one system broke every other system that directly accessed its internals.

APIs introduce a layer of indirection. The server exposes specific operations through a defined interface. The client uses only that interface, never touching internals. When the server's internal implementation changes, the interface stays stable. The client breaks only if the interface changes, not every time the server changes its internal structure.

This contract model scales. One API can serve thousands of different clients: the iOS app, the Android app, the web application, third-party developers, internal tools. All of them talk through the same interface. The server team can refactor, optimize, or completely rewrite internals without breaking every client simultaneously.

HTTP: The Foundation

Most modern APIs use HTTP as their transport protocol. HTTP (HyperText Transfer Protocol) is the protocol your browser uses to load web pages. It is a request-response protocol: a client sends a request, a server sends back a response.

An HTTP request has:

  • A method (verb): what action you want to perform
  • A URL: what resource you are acting on
  • Headers: metadata about the request (content type, authentication, etc.)
  • A body: the request payload (optional, depends on method)

An HTTP response has:

  • A status code: a numeric indicator of what happened
  • Headers: metadata about the response
  • A body: the response payload

HTTP methods define the intended operation:

Method Meaning Has Body Idempotent
GET Retrieve a resource No Yes
POST Create a resource or trigger an action Yes No
PUT Replace a resource entirely Yes Yes
PATCH Update part of a resource Yes No
DELETE Remove a resource No Yes

Idempotent means calling the operation multiple times produces the same result as calling it once. GET /products/42 always returns the same product (assuming it exists). DELETE /products/42 deletes the product the first time; subsequent calls find nothing to delete. POST /orders creates a new order each time it is called, which is why it is not idempotent.

HTTP status codes communicate what happened:

Code Meaning When to Use
200 OK Successful GET, PUT, PATCH
201 Created Successful POST that created a resource
204 No Content Successful DELETE, or action with no response body
400 Bad Request Invalid request data, validation errors
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but not permitted
404 Not Found Resource does not exist
409 Conflict State conflict (duplicate, version mismatch)
422 Unprocessable Entity Syntactically valid but semantically invalid
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Something broke on the server
503 Service Unavailable Server is down or overloaded

Using status codes correctly is the difference between an API that is easy to debug and one that is opaque. An API that returns 200 with {"error": "not found"} in the body forces clients to parse the body to detect errors instead of using the status code. Every HTTP client library understands status codes. Use them.

REST: The Dominant API Design Style

REST (Representational State Transfer) is an architectural style for designing networked APIs. Roy Fielding defined it in his 2000 PhD dissertation. It is not a standard or a specification. It is a set of constraints that, when followed, produce APIs with predictable, scalable behavior.

The core constraints:

Client-Server separation: The client and server are independent. The server does not know about the client's UI. The client does not know about the server's implementation.

Statelessness: Each request must contain all the information needed to process it. The server does not store client session state between requests. Authentication credentials are included in every request, not stored server-side per session.

Uniform interface: Resources are identified by URLs. Resources are manipulated through representations (typically JSON). Messages are self-descriptive. (HATEOAS - hypermedia as the engine of application state - is the fourth constraint that most APIs ignore.)

Layered system: Clients do not need to know whether they are talking directly to the server or through a proxy, cache, or load balancer.

Cacheability: Responses should indicate whether they can be cached. Cacheable responses improve performance by letting clients and intermediaries avoid redundant requests.

In practice, most APIs described as "RESTful" follow the first three constraints and ignore HATEOAS. That is fine. The practical benefit of REST is the uniform interface: resources have URLs, standard HTTP methods operate on them, status codes communicate results, and JSON carries data. These conventions let any developer understand an unfamiliar API quickly.

REST API Design Principles

Good API design is harder than it looks. The decisions made on day one become load-bearing walls: changing them breaks existing clients.

URL structure represents resources, not actions.

Bad:
GET  /getProduct?id=42
POST /createOrder
POST /deleteProduct?id=42
 
Good:
GET    /products/42
POST   /orders
DELETE /products/42

URLs identify resources (nouns). HTTP methods identify operations (verbs). GET /products/42 means "get the product with ID 42." DELETE /products/42 means "delete the product with ID 42." The action is in the method, not the URL.

Nested resources show relationships.

GET  /users/123/orders           -- orders for user 123
GET  /users/123/orders/456       -- specific order for user 123
GET  /products/42/reviews        -- reviews for product 42
POST /products/42/reviews        -- create a review for product 42

Nesting beyond two levels gets unwieldy. /users/123/orders/456/items/7/product is too deep. At that depth, flatten the structure and use query parameters or IDs.

Consistent response shapes.

{
    "data": {
        "id": 42,
        "name": "Wireless Headphones",
        "price": 79.99
    }
}
 
{
    "data": [
        { "id": 1, "name": "Product A" },
        { "id": 2, "name": "Product B" }
    ],
    "pagination": {
        "page": 1,
        "per_page": 20,
        "total": 143,
        "pages": 8
    }
}
 
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Request validation failed",
        "details": [
            { "field": "price", "message": "Price must be greater than 0" },
            { "field": "name", "message": "Name is required" }
        ]
    }
}

The data wrapper for success responses and error wrapper for error responses gives clients a consistent top-level structure to check. The pagination object on list responses gives clients everything they need to implement pagination controls.

Versioning from day one.

/api/v1/products
/api/v2/products

API versioning in the URL path is pragmatic. When you need to make a breaking change to the API (changing a field name, removing a field, changing a data type), you create a new version. Old clients continue using v1. New clients use v2. You run both versions simultaneously until old client traffic drops to zero, then deprecate v1.

Alternative versioning strategies exist: header-based versioning (Accept: application/vnd.myapi.v2+json) and query parameter versioning (?version=2). URL path versioning is the most visible and least likely to be accidentally dropped by clients or intermediaries.

Filtering, sorting, and pagination through query parameters.

GET /products?status=active&category=electronics&min_price=50&max_price=200
GET /products?search=wireless+headphones
GET /products?sort=price&order=asc
GET /products?page=2&per_page=20

Never return an entire collection without pagination. An endpoint that returns all products works fine with 100 products and breaks catastrophically with 10 million. Default to a reasonable page size (20-50), enforce a maximum (100-200), and always include total count and page metadata in the response.

Authentication

APIs need to verify who is making requests. Three primary mechanisms:

API Keys are simple strings that identify the caller. The client includes the key in a header or query parameter. The server looks up the key to identify which application is making the request.

Authorization: Bearer api_key_abc123xyz
X-API-Key: api_key_abc123xyz

API keys are appropriate for server-to-server communication where the key is stored securely on the server side. They are not appropriate for mobile or browser clients where the key would be visible to end users.

JWT (JSON Web Tokens) are self-contained tokens containing encoded claims about the user. The server issues a signed JWT on successful login. The client includes the JWT in every subsequent request. The server verifies the signature without a database lookup, because the token itself contains the user's ID and permissions.

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT structure is three base64url-encoded parts separated by dots: header (algorithm and token type), payload (claims: user ID, expiry, roles), and signature. Anyone can decode the header and payload (they are just base64). Only the server with the secret key can verify the signature.

JWT is appropriate for user-facing APIs. The access token expires quickly (15 minutes to 1 hour). A longer-lived refresh token (days to weeks) is used to obtain new access tokens without re-authentication.

OAuth 2.0 is the standard for delegated authorization. When you click "Sign in with Google" on a third-party site, you are using OAuth. You authorize the third-party app to access specific resources in your Google account. Google issues a token to the app. The app uses that token to access only the resources you authorized. You never gave the third-party app your Google password.

OAuth is complex and implementing it correctly from scratch is error-prone. Use a library or an identity provider (Auth0, Clerk, Supabase Auth) for OAuth flows rather than rolling your own.

Rate Limiting

Unprotected APIs get hammered. Bot traffic, misconfigured clients in infinite retry loops, and occasionally deliberate abuse will exhaust your server capacity without rate limiting.

Rate limiting restricts how many requests a client can make in a given time window. Common strategies:

Fixed window: Count requests in a fixed time window (e.g., 100 requests per minute). Reset the counter at each window boundary. Simple but vulnerable to burst traffic at window boundaries.

Sliding window: Track request timestamps in a rolling window. More accurate than fixed window, slightly more expensive to compute.

Token bucket: Each client has a bucket of tokens. Each request consumes a token. Tokens refill at a fixed rate. Allows burst traffic up to the bucket size before rate limiting kicks in.

Rate limit headers communicate the current state to clients:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1704067200
Retry-After: 30

X-RateLimit-Limit is the maximum requests per window. X-RateLimit-Remaining is how many requests the client has left in the current window. X-RateLimit-Reset is the Unix timestamp when the window resets. Retry-After appears on 429 responses and tells the client how many seconds to wait before retrying.

Well-designed clients respect rate limit headers and back off gracefully. Clients that retry immediately on 429 responses amplify the problem they are trying to solve.

API Design Patterns and Anti-Patterns

Returning consistent error responses. Every error should return a structured JSON body with a machine-readable error code and a human-readable message. Returning HTML error pages (the default for many web frameworks) is not acceptable for an API.

{
    "error": {
        "code": "RESOURCE_NOT_FOUND",
        "message": "Product with ID 42 does not exist",
        "request_id": "req_abc123"
    }
}

The request_id is a unique identifier for the request that appears in your logs. When a client reports an error, they give you the request ID and you find the exact log entry immediately. This is enormously useful in production debugging.

Idempotency keys for non-idempotent operations. Payment APIs and order APIs face a problem: what happens when the client sends a request, the server processes it successfully, but the response is lost in transit? The client does not know whether the server received the request. Retrying creates a duplicate.

Idempotency keys solve this. The client generates a unique key for each operation and includes it in a header. The server stores the result mapped to that key. If the same key arrives again, the server returns the stored result instead of processing the request again.

Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000

Stripe pioneered this pattern for payment APIs. Any operation that creates side effects (charges, orders, sends emails) should support idempotency keys.

Partial responses for bandwidth efficiency.

GET /products?fields=id,name,price

Mobile clients on slow connections do not need a product's full 2KB description when displaying a list of product names and prices. A fields parameter that specifies which fields to return reduces response size significantly for list endpoints.

Webhooks for push-based notifications. Polling is inefficient. A client checking "did anything change?" every 30 seconds sends thousands of requests per day getting "no" for an answer. Webhooks reverse the direction: the server sends an HTTP POST to a URL you provide whenever an event occurs.

POST https://yourapp.com/webhooks/payments
 
{
    "event": "payment.completed",
    "data": {
        "payment_id": "pay_abc123",
        "amount": 4999,
        "currency": "USD",
        "customer_id": "cust_xyz789"
    },
    "timestamp": "2024-01-15T14:30:00Z",
    "signature": "sha256=abc123..."
}

The signature header lets you verify the webhook came from the service you expect and was not tampered with. Compute HMAC-SHA256 of the request body using a shared secret and compare it to the signature header.

GraphQL: When REST is Not Enough

REST has a problem called over-fetching and under-fetching. A product detail endpoint returns the product with its full description, variants, images, and metadata. A product list endpoint returns the same heavy object times twenty. You need a lighter response for the list. You end up either adding a fields parameter, creating separate endpoints, or accepting inefficiency.

GraphQL solves this. The client specifies exactly what fields it wants in the query. The server returns exactly those fields and nothing else.

query GetProductsWithBasicInfo {
    products(status: ACTIVE, limit: 20) {
        id
        name
        price
        category {
            name
        }
    }
}
 
query GetProductDetail($slug: String!) {
    product(slug: $slug) {
        id
        name
        description
        price
        stock
        category {
            name
            slug
        }
        reviews(limit: 5) {
            rating
            title
            user {
                username
            }
        }
    }
}
 
mutation CreateOrder($input: CreateOrderInput!) {
    createOrder(input: $input) {
        id
        totalAmount
        status
        items {
            product {
                name
            }
            quantity
            unitPrice
        }
    }
}

The first query fetches only id, name, price, and category name. The second fetches the full detail including description, reviews, and nested user info. The server resolves exactly what was requested.

GraphQL's tradeoffs: complexity shifts from the server (maintaining multiple REST endpoints) to the client (constructing queries). N+1 query problems are easy to create (fetching the user field on every review triggers a separate DB query per review without careful resolver implementation using DataLoader). Caching is harder because every query is a POST request to the same URL. For internal APIs consumed by a small number of controlled clients, GraphQL's flexibility is often worth the tradeoffs. For public APIs with hundreds of different clients, REST's simplicity and cacheability often wins.

gRPC: APIs Between Services

REST and GraphQL are designed for browser and mobile clients. For service-to-service communication in a backend infrastructure, gRPC is increasingly common.

gRPC uses Protocol Buffers (protobuf) for serialization and HTTP/2 for transport. Protobuf is a binary serialization format: smaller than JSON, faster to serialize and deserialize, and schema-defined. You define your service and message types in a .proto file. The gRPC toolchain generates client and server code in your language.

syntax = "proto3";
 
package products;
 
service ProductService {
    rpc GetProduct (GetProductRequest) returns (Product);
    rpc ListProducts (ListProductsRequest) returns (ListProductsResponse);
    rpc CreateProduct (CreateProductRequest) returns (Product);
    rpc StreamProductUpdates (StreamRequest) returns (stream ProductUpdate);
}
 
message Product {
    int64 id = 1;
    string name = 2;
    string slug = 3;
    double price = 4;
    int32 stock = 5;
    string status = 6;
    int64 created_at = 7;
}
 
message GetProductRequest {
    string slug = 1;
}
 
message ListProductsRequest {
    string status = 1;
    int32 category_id = 2;
    int32 page = 3;
    int32 per_page = 4;
}
 
message ListProductsResponse {
    repeated Product products = 1;
    int32 total = 2;
    int32 page = 3;
    int32 pages = 4;
}
 
message CreateProductRequest {
    string name = 1;
    string description = 2;
    double price = 3;
    int32 stock = 4;
    int32 category_id = 5;
}
 
message StreamRequest {}
 
message ProductUpdate {
    string event_type = 1;
    Product product = 2;
    int64 timestamp = 3;
}

gRPC's advantages over REST for service-to-service: binary protocol is faster and smaller than JSON, strongly typed contracts prevent entire categories of integration bugs, streaming (both request and response) is a first-class feature, and generated client code eliminates hand-rolling HTTP clients. The disadvantage is that gRPC is not natively supported in browsers (requires a gRPC-Web proxy), making it unsuitable for browser-facing APIs without an HTTP translation layer.

Large-scale microservice architectures (Google, Square, Netflix, Uber) use gRPC heavily for internal service communication where performance and type safety matter more than browser compatibility.

API Security

Security is not a feature you add to an API. It is the architecture you build from the start. The most common API vulnerabilities appear in specific, predictable places.

Injection attacks. SQL injection, command injection, and template injection all follow the same pattern: user-supplied input is interpreted as code instead of data. The fix is parameterized queries (never string-concatenated SQL), input validation before processing, and the principle of least privilege on database accounts.

const slug = req.params.slug;
 
const bad = `SELECT * FROM products WHERE slug = '${slug}'`;
 
const good = 'SELECT * FROM products WHERE slug = $1';
const result = await db.query(good, [slug]);

A request with slug = "'; DROP TABLE products; --" executes the injected SQL in the bad version. It inserts a literal string in the good version.

Broken object level authorization (BOLA). The most common authorization failure in REST APIs. A user with ID 123 sends GET /orders/456. The API returns the order without checking whether order 456 belongs to user 123. Now the user can enumerate any order in the system.

const order = await db.queryOne(
    'SELECT * FROM orders WHERE id = $1',
    [req.params.id]
);
 
if (!order) throw new NotFoundError('Order');
 
if (order.user_id !== req.user.id && !req.user.is_admin) {
    throw new ForbiddenError();
}

Always check ownership. Every endpoint that operates on a resource owned by a user must verify that the requesting user owns that resource. This is different from authentication (proving who you are) and different from role-based access control (checking your role). It is object-level authorization.

Excessive data exposure. An API endpoint returning the full user object including password hash, internal flags, and administrative fields is a data exposure vulnerability. Return only what the client needs.

const user = await db.queryOne('SELECT * FROM users WHERE id = $1', [id]);
 
const safeUser = {
    id:         user.id,
    username:   user.username,
    email:      user.email,
    created_at: user.created_at,
};

Mass assignment. Accepting a JSON body and passing it directly to a database update without whitelisting allowed fields lets clients set fields they should not control.

await db.query(
    'UPDATE users SET $1 WHERE id = $2',
    [req.body, req.user.id]
);
 
const allowed = ['username', 'email', 'bio'];
const updates: Record<string, unknown> = {};
for (const key of allowed) {
    if (key in req.body) updates[key] = req.body[key];
}

Rate limiting and brute force protection. Authentication endpoints without rate limiting allow brute force attacks. Ten requests per minute to /login means a client can try 14,400 passwords per day against a single account. A strong rate limit (5-10 per minute per IP on auth endpoints) makes brute force attacks impractical.

HTTPS everywhere. APIs served over HTTP expose tokens and credentials to anyone on the network path. Every production API must use HTTPS. Redirect HTTP to HTTPS at the reverse proxy level. Set Strict-Transport-Security headers so browsers remember to use HTTPS for your domain.

Caching Strategies for APIs

The right caching strategy reduces database load, improves response times, and handles traffic spikes that would otherwise overwhelm your database.

HTTP caching uses response headers to instruct clients and intermediaries (CDNs, proxies) how long to cache responses. For public, read-heavy endpoints with data that changes infrequently, HTTP caching eliminates database queries entirely for cached responses.

Cache-Control: public, max-age=300, stale-while-revalidate=60
ETag: "abc123"
Last-Modified: Mon, 15 Jan 2024 10:00:00 GMT

max-age=300 tells the client to use the cached response for 5 minutes. stale-while-revalidate=60 says the client can serve the stale response for an additional 60 seconds while fetching a fresh one in the background. The user sees a fast response. The cache is refreshed asynchronously.

ETag is a hash of the response content. The client sends If-None-Match: "abc123" on subsequent requests. If the content has not changed, the server returns 304 Not Modified with no body. The client uses its cached version. Database was queried, but no data transferred.

Application-level caching with Redis stores computed results for expensive queries.

const cacheKey = `product:${slug}`;
const cached   = await redis.get(cacheKey);
 
if (cached) {
    return res.json({ data: JSON.parse(cached), cached: true });
}
 
const product = await db.queryOne('SELECT * FROM products WHERE slug = $1', [slug]);
if (!product) throw new NotFoundError('Product', slug);
 
await redis.setex(cacheKey, 300, JSON.stringify(product));
res.json({ data: product });

Cache invalidation is the hard part. When a product is updated, the cached version is stale. You need to delete or update the cache entry on every write that changes the cached data. Missing an invalidation case means users see stale data until the cache TTL expires. The simplest rule: always invalidate explicitly on write, never rely solely on TTL expiry for data accuracy.

Versioning Strategies in Depth

URL path versioning (/api/v1/) is the most common approach and the one I recommend. It is explicit, visible in logs, and survives every HTTP layer from load balancers to client caches.

When you make a breaking change, you create v2 and run both in parallel:

/api/v1/products  -- old shape, old clients
/api/v2/products  -- new shape, new clients

v1 and v2 handlers can share the same underlying service code. The version layer handles transforming the shared internal representation to the contract each version promises.

Non-breaking changes do not require a version bump. Adding a new optional field to a response is non-breaking. Adding a new optional request parameter is non-breaking. Removing a field, changing a field's type, or changing the structure of a response is breaking.

Document your deprecation policy. "v1 will be supported until December 31, 2024, after which it will be removed" gives API consumers time to migrate. Without a deprecation timeline, old API versions accumulate indefinitely because no one knows it is safe to remove them.

OpenAPI and API Documentation

An API without documentation is useless to anyone who did not build it. OpenAPI (formerly Swagger) is the standard for documenting REST APIs. You describe your API in a YAML or JSON file that follows the OpenAPI specification. Tools generate interactive documentation, client SDKs, and server stubs from that file.

openapi: 3.1.0
info:
  title: Product API
  description: API for managing products and orders
  version: 1.0.0
 
servers:
  - url: https://api.example.com/v1
    description: Production
 
security:
  - bearerAuth: []
 
paths:
  /products:
    get:
      summary: List products
      operationId: listProducts
      tags: [Products]
      security: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, active, archived]
            default: active
        - name: category
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Paginated product list
          content:
            application/json:
              schema:
                type: object
                required: [data, pagination]
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Product'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
 
    post:
      summary: Create a product
      operationId: createProduct
      tags: [Products]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProductRequest'
      responses:
        '201':
          description: Product created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Product'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
 
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
 
  schemas:
    Product:
      type: object
      required: [id, name, slug, price, stock, status, created_at]
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
          maxLength: 255
        slug:
          type: string
          maxLength: 255
        price:
          type: number
          format: double
          minimum: 0.01
        stock:
          type: integer
          minimum: 0
        status:
          type: string
          enum: [draft, active, archived]
        created_at:
          type: string
          format: date-time
 
    CreateProductRequest:
      type: object
      required: [name, description, price, category_id]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          minLength: 1
        price:
          type: number
          minimum: 0.01
        stock:
          type: integer
          minimum: 0
          default: 0
        category_id:
          type: integer
 
    Pagination:
      type: object
      required: [page, per_page, total, pages]
      properties:
        page:
          type: integer
        per_page:
          type: integer
        total:
          type: integer
        pages:
          type: integer
 
  responses:
    ValidationError:
      description: Validation failed
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  details:
                    type: array
                    items:
                      type: object
                      properties:
                        field:
                          type: string
                        message:
                          type: string
 
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code:
                    type: string
                  message:
                    type: string

OpenAPI documents are living documentation. Keep them in your repository alongside your code. Tools like Swagger UI render them as interactive documentation where developers can browse endpoints, see request/response schemas, and test calls directly from the browser. The next article in this series covers building a complete TypeScript API from scratch, and the code generates OpenAPI documentation automatically from route definitions.

What an API Is Not

An API is not just a REST endpoint. The term covers any programmatic interface between software components. Database drivers expose an API. Libraries expose an API. The DOM exposes an API to JavaScript. When developers say "the JavaScript Event Loop is part of the browser's API surface," they mean the interface through which JavaScript code interacts with the browser's concurrency model.

An API is not inherently public. Most APIs are internal: services in a backend architecture communicating with each other. Public APIs, which third-party developers can build on, are a subset with additional constraints around backward compatibility, documentation quality, and change management.

An API is not a protocol. REST is a style. HTTP is the protocol REST APIs typically run over. gRPC is a framework that uses HTTP/2 as its protocol. WebSockets are a protocol for bidirectional real-time communication. MQTT is a protocol for IoT messaging. The API is the contract. The protocol is the communication mechanism.

Understanding APIs is understanding how modern software is composed. Services delegate to other services. Mobile apps consume backend APIs. Frontend applications call backend APIs that call third-party APIs. The entire software supply chain is a network of APIs, and every API is a contract that makes it possible for independent teams to build independent components that work together.

API Testing

An API without tests is a promise with no enforcement. Testing verifies that your API behaves as documented, catches regressions before deployment, and documents expected behavior more precisely than any written specification.

Unit tests test individual functions in isolation. Validation logic, business rules, data transformations. Fast to run. Narrow in scope.

Integration tests send actual HTTP requests against your API and verify the full response including status code, headers, and body. These tests go through your entire stack: middleware, route handlers, validation, business logic, and database queries. Slower than unit tests but catch a different category of bugs.

describe('POST /api/v1/orders', () => {
    it('creates an order and decrements stock', async () => {
        const response = await request(app)
            .post('/api/v1/orders')
            .set('Authorization', `Bearer ${token}`)
            .send({
                items: [{ product_id: 1, quantity: 2 }],
                shipping_address: {
                    name: 'Test User',
                    street: '123 Main St',
                    city: 'Testville',
                    country: 'US',
                    postal: '12345'
                }
            });
 
        expect(response.status).toBe(201);
        expect(response.body.data.status).toBe('pending');
        expect(response.body.data.items).toHaveLength(1);
 
        const product = await db.queryOne(
            'SELECT stock FROM products WHERE id = $1', [1]
        );
        expect(product.stock).toBe(initialStock - 2);
    });
 
    it('returns 400 when stock is insufficient', async () => {
        const response = await request(app)
            .post('/api/v1/orders')
            .set('Authorization', `Bearer ${token}`)
            .send({
                items: [{ product_id: 1, quantity: 9999 }],
                shipping_address: { name: 'Test', street: '123', city: 'City', country: 'US', postal: '12345' }
            });
 
        expect(response.status).toBe(400);
        expect(response.body.error.code).toBe('VALIDATION_ERROR');
    });
});

Contract tests verify that your API matches its OpenAPI specification. Tools like Dredd or Schemathesis send generated requests based on your spec and verify that responses match the schema. They catch the divergence between documented and actual behavior that accumulates when developers update code but not documentation.

Load tests verify that your API handles the expected traffic volume without degradation. Tools like k6, Locust, and Apache JMeter simulate concurrent users and measure response times and error rates under load. Run load tests before major product launches, not after the first time the site goes down under traffic.

The full article on building a production TypeScript API from scratch, including a complete test suite, is here.