Understand how applications communicate through APIs, including HTTP, RESTful design, requests and responses, status codes, authentication, pagination, errors, caching, versioning, GraphQL queries and mutations, and secure client-server boundaries.
Explain how clients and servers communicate through APIs
Build and consume HTTP-based APIs
Apply RESTful resource and method conventions
Use status codes, headers, query parameters, and request bodies correctly
Handle authentication, pagination, validation, errors, and caching
Understand GraphQL schemas, queries, mutations, variables, and resolvers
Compare RESTful APIs and GraphQL based on application requirements
An API defines how software systems communicate. In a web application, the browser often acts as a client that sends requests to a server exposing application capabilities.
The API boundary separates the interface used by callers from the implementation behind that interface.
APIs expose defined capabilities to callers
Clients should depend on the API contract rather than internal implementation
Servers remain responsible for trusted business rules and authorization
async function loadProfile() {
const response = await fetch("/api/profile", {
headers: {
Accept: "application/json",
},
});
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response.json();
}HTTP communication is organized around requests and responses. A request includes a method, target URL, headers, and optionally a body.
The response includes a status code, headers, and optionally a response body.
HTTP methods communicate request intent
Headers carry metadata
Bodies commonly carry JSON or other representations
Status codes communicate the outcome of a request
const response = await fetch("/api/projects", {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
name: "Frontend Practice",
}),
});
const project = await response.json();RESTful APIs commonly organize endpoints around resources represented by nouns such as users, projects, orders, or comments.
HTTP methods communicate operations on those resources instead of placing action verbs into every endpoint.
Model endpoints around resources
Use nouns in resource paths
Use HTTP methods to communicate operation intent
Keep resource naming predictable
GET /api/projects
GET /api/projects/42
POST /api/projects
PATCH /api/projects/42
DELETE /api/projects/42
Nested resource example:
GET /api/projects/42/commentsGET retrieves a representation, POST commonly creates a resource or submits a command, PUT replaces a resource representation, PATCH applies a partial update, and DELETE requests removal.
Method semantics help clients, intermediaries, documentation, and developers understand the intent of a request.
GET should not intentionally mutate application state
POST commonly creates or processes
PUT commonly represents complete replacement
PATCH commonly represents partial modification
await fetch("/api/users/42", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
displayName: "Ada",
}),
});HTTP status codes communicate broad request outcomes. Successful responses use 2xx codes, client-related failures use 4xx codes, and server failures use 5xx codes.
Choosing meaningful status codes helps callers distinguish different outcomes without parsing arbitrary message strings.
200 commonly represents a successful request
201 commonly represents successful resource creation
400 indicates an invalid client request
401 indicates missing or invalid authentication
403 indicates insufficient permission
404 indicates a resource was not found
if (!session) {
return Response.json(
{ error: "Authentication required" },
{ status: 401 },
);
}
const project = await findProject(projectId);
if (!project) {
return Response.json(
{ error: "Project not found" },
{ status: 404 },
);
}
return Response.json(project, { status: 200 });Path parameters typically identify a specific resource, while query parameters commonly control filtering, sorting, searching, or pagination.
Request bodies carry structured input for operations such as creation or modification.
Use path parameters for resource identity
Use query parameters for optional request controls
Use request bodies for structured creation or update data
Validate all incoming values
Resource identity:
GET /api/users/42
Filtering and pagination:
GET /api/users?role=admin&page=2
Structured update:
PATCH /api/users/42
{
"displayName": "Ada"
}API inputs should be treated as untrusted. The server must validate the shape and allowed values of request bodies, parameters, and query strings.
Runtime schema validation can keep malformed data from reaching business logic or persistence layers.
Validate at the server boundary
Do not rely exclusively on frontend validation
Return useful validation errors
Keep transport validation separate from deeper business rules where appropriate
import { z } from "zod";
const CreateProjectSchema = z.object({
name: z.string().trim().min(1).max(100),
visibility: z.enum(["private", "public"]),
});
const body = await request.json();
const result = CreateProjectSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: "Invalid request" },
{ status: 400 },
);
}Authentication determines the identity associated with a request. Authorization determines whether that identity has permission to perform the requested operation.
API authorization must be enforced on the server even when the frontend hides controls from unauthorized users.
Authenticate the caller
Authorize each protected operation
Never treat frontend visibility as a security boundary
Apply least privilege
const user = await requireUser(request);
const project = await findProject(projectId);
if (!project) {
return Response.json(
{ error: "Not found" },
{ status: 404 },
);
}
if (project.ownerId !== user.id) {
return Response.json(
{ error: "Forbidden" },
{ status: 403 },
);
}API errors should be predictable enough for clients to handle programmatically while still providing useful diagnostic information.
Avoid leaking sensitive implementation details such as stack traces, SQL statements, secrets, or internal infrastructure information.
Use consistent error structures
Provide machine-readable error codes when useful
Do not expose sensitive internals
Log deeper diagnostic information on trusted systems
{
"error": {
"code": "VALIDATION_FAILED",
"message": "The submitted project is invalid.",
"fields": {
"name": "Name is required."
}
}
}Large collections should usually be returned in smaller pages instead of sending every record in one response.
APIs can expose filtering and sorting controls while keeping their syntax predictable and documented.
Paginate large collections
Use explicit filtering parameters
Define stable sorting behavior
Cursor pagination can be useful for frequently changing datasets
GET /api/projects
?status=active
&sort=created_at
&order=desc
&limit=20
&cursor=eyJpZCI6NDJ9HTTP provides caching mechanisms that can reduce repeated network transfers and server work.
Cache-Control, validators such as ETag, and conditional requests allow clients and intermediaries to determine whether a previously retrieved representation can be reused.
Cache only when the resource semantics allow it
Cache-Control communicates caching policy
ETags can support conditional requests
Sensitive personalized responses require careful caching rules
return Response.json(publicCourses, {
headers: {
"Cache-Control":
"public, max-age=60, s-maxage=300",
},
});APIs become dependencies for clients, so changing an existing contract can break applications that depend on it.
Versioning strategies and backward-compatible evolution help APIs change without unnecessarily disrupting callers.
Avoid unnecessary breaking changes
Add optional fields more safely than removing required fields
Version deliberately when contracts must diverge
Deprecate old behavior before removing it when practical
GET /api/v1/users/42
Later breaking contract:
GET /api/v2/users/42
A new version should represent a meaningful
contract change rather than every small release.GraphQL APIs expose a typed schema describing the fields and operations clients can request.
The schema acts as a contract between clients and the server and defines object types, fields, arguments, and available root operations.
GraphQL APIs are schema-driven
Clients request specific fields
Schemas define available types and operations
The type system enables validation and tooling
type User {
id: ID!
name: String!
email: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
updateUserName(
id: ID!
name: String!
): User!
}A GraphQL query specifies exactly which fields a client wants returned. Nested fields can express relationships within the same operation.
Variables keep dynamic values separate from query text and make operations easier to reuse.
Queries request fields explicitly
Nested selections request related data
Use variables for dynamic input
Avoid embedding arbitrary user values directly into query strings
query UserProfile($id: ID!) {
user(id: $id) {
id
name
email
projects {
id
name
}
}
}
# Variables
{
"id": "42"
}GraphQL mutations represent operations that change server-side state. Mutations accept arguments or input objects and return fields selected by the client.
The server remains responsible for validating input, authenticating the request, and authorizing the requested change.
Mutations represent state-changing operations
Input types organize mutation arguments
Clients select the mutation response fields
Mutation authorization belongs on the server
mutation RenameProject(
$input: RenameProjectInput!
) {
renameProject(input: $input) {
id
name
updatedAt
}
}
# Variables
{
"input": {
"projectId": "42",
"name": "Frontend Lab"
}
}Resolvers are server-side functions that provide values for GraphQL fields. They connect schema operations to application services, databases, and other data sources.
Careless nested resolution can create repeated database requests, commonly called the N+1 query problem.
Resolvers implement schema fields
Resolvers should delegate business logic to appropriate application layers
Batching and caching can reduce repeated data access
Authorization must still be enforced during resolution
const resolvers = {
Query: {
user: async (
_parent: unknown,
args: { id: string },
context: GraphQLContext,
) => {
const currentUser = await context.requireUser();
return context.users.findVisibleById(
currentUser,
args.id,
);
},
},
};REST and GraphQL are different approaches to API design. REST commonly exposes multiple resource-oriented HTTP endpoints, while GraphQL commonly exposes a typed graph through a query language.
Neither approach is universally better. The best choice depends on client needs, caching strategy, operational complexity, schema requirements, and the shape of the domain.
REST maps naturally to HTTP resources and methods
GraphQL lets clients select fields through a typed schema
GraphQL can reduce over-fetching for complex client data needs
REST can provide simpler HTTP-level caching and operational behavior
Choose based on system requirements rather than popularity
REST:
GET /api/users/42
GET /api/users/42/projects
GraphQL:
query {
user(id: "42") {
name
projects {
id
name
}
}
}