Skip to main content

Other API theory

What are the main differences between REST and GraphQL​

REST (Representational State Transfer):

  • Resource-Based: REST APIs are organized around resources, identified by URIs. Each resource corresponds to a specific endpoint (e.g., /users or /orders).
  • Multiple Endpoints: Each resource typically has its own endpoint, and multiple endpoints are used to perform different operations (e.g., /users, /orders).
  • HTTP Methods: Uses standard HTTP methods (GET, POST, PUT, DELETE) to perform operations on resources.
  • Overfetching/Underfetching: REST responses often include more data than necessary (overfetching) or not enough data, requiring multiple requests (underfetching).
  • Versioning: Versioning is typically done through the URI (e.g., /api/v1/users), which can lead to multiple versions of the same resource.
  • Stateless: Each request is independent and does not retain client state on the server between requests.
  • Cacheable: Responses can be cached to improve performance.

GraphQL:

  • Single Endpoint: GraphQL APIs use a single endpoint (e.g., /graphql) where all operations are performed.
  • Client-Specified Queries: Clients specify the exact structure of the response, asking for only the data they need. This reduces overfetching and underfetching.
  • Schema-Driven: The API is defined by a schema that specifies available types, queries, and mutations.
  • No Versioning: Because clients request exactly what they need, GraphQL APIs typically don’t require versioning.
  • Nested Queries: GraphQL allows clients to retrieve nested and related data in a single request.
  • Real-Time Updates: Supports real-time data through subscriptions, allowing clients to receive updates when data changes.

Key Differences:

  • Efficiency: GraphQL can be more efficient because it reduces overfetching and underfetching by allowing clients to request only the data they need.
  • Flexibility: GraphQL offers more flexibility in data retrieval, allowing for complex queries and nested data retrieval in a single request.
  • Complexity: GraphQL can be more complex to set up and manage due to the need for a detailed schema and resolvers.
  • Tooling: REST is more established and has broader support, but GraphQL is gaining popularity with tools like Apollo and Relay.

How do you implement pagination, filtering, and sorting in a REST API​

Pagination
Pagination helps manage large datasets by breaking them into manageable chunks, reducing the load on the server and improving performance for clients.

  • Offset-Based Pagination:
    Use query parameters like limit and offset to specify the number of records to return and where to start.
GET /api/users?limit=10&offset=20

The above example returns 10 users starting from the 21st user.

  • Cursor-Based Pagination: Use a cursor (usually an opaque string representing the position of an item in the dataset) to navigate through pages.
GET /api/users?cursor=eyJpZCI6MjB9&limit=10

This method is more performant for large datasets as it avoids the need to calculate offsets.

  • Response Metadata
    Include metadata in the response to provide information about pagination (e.g., total count, next cursor).
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MjB9",
"totalCount": 100
}
}

Use query parameters to specify filters.

Filtering
Filtering allows clients to retrieve only the data that matches certain criteria.

  • Query Parameters
GET /api/users?age=30&status=active

This request fetches users who are 30 years old and have an active status.

  • Complex Filters
    For more complex filters, use a combination of query parameters or a JSON payload for POST requests.
GET /api/products?price[gte]=20&price[lte]=50

Sorting
Sorting allows clients to specify the order in which the results should be returned.

  • Sort Query Parameter
    Use a sort query parameter to specify sorting criteria.
GET /api/users?sort=createdAt,desc

This request sorts users by the createdAt field in descending order.

  • Multiple Sorting Fields
    Support sorting by multiple fields by accepting a comma-separated list of fields.
GET /api/products?sort=price,asc&sort=rating,desc

Combined Example

GET /api/products?category=electronics&price[gte]=100&sort=price,asc&limit=20&offset=0

This example fetches electronics products priced at $100 or more, sorted by price in ascending order, and returns the first 20 results.

What is CORS (Cross-Origin Resource Sharing) and how is it used in the context of REST APIs​

CORS (Cross-Origin Resource Sharing) is a security feature implemented by web browsers that restricts how resources on a web page can be requested from a different domain, protocol, or port than the one from which the resource originated.

  • Why CORS is Important:
    Security: CORS prevents potentially malicious websites from making unauthorized requests to your API from a different origin.
    Controlled Sharing: It allows servers to specify who can access their resources and which HTTP methods are allowed.
  • How CORS Works
    Preflight Requests
    For HTTP methods other than GET, POST, or HEAD, or for certain custom headers, the browser sends a preflight request (an OPTIONS request) to the server to determine if the actual request is safe to send.
    The server responds with the allowed methods and headers.
    CORS Headers
    The server uses specific headers to indicate which origins are allowed to access the resource
    Access-Control-Allow-Origin: Specifies which origins are allowed. Can be a specific origin or * to allow all.
Access-Control-Allow-Origin: https://example.com

Access-Control-Allow-Methods: Specifies the allowed HTTP methods (e.g., GET, POST, PUT).

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: Specifies which headers can be used in the actual request.

Access-Control-Allow-Headers: Content-Type, Authorization

Access-Control-Allow-Credentials: Indicates whether the browser should include credentials (like cookies) in requests to the API.

Access-Control-Allow-Credentials: true
  • Example Configuration In an Express.js server, you can use the cors middleware to handle CORS
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
origin: 'https://example.com',
methods: 'GET,POST,PUT,DELETE',
allowedHeaders: 'Content-Type,Authorization',
credentials: true,
};

app.use(cors(corsOptions));

app.get('/api/data', (req, res) => {
res.json({ message: 'This is CORS-enabled for https://example.com' });
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

What are the best practices for developing a REST API​

  1. Use Proper HTTP Methods: Follow REST conventions for using HTTP methods (GET, POST, PUT, DELETE, PATCH) for the appropriate operations.
  2. Consistent Naming Conventions: Use clear, consistent, and meaningful names for your endpoints, typically using nouns for resources.
GET /api/users          // Fetches all users
POST /api/users // Creates a new user
  1. Statelessness: Ensure that each request is stateless, meaning it should contain all the information the server needs to process it.
  2. Versioning: Use versioning in your API to manage changes and maintain backward compatibility.
GET /v1/api/users
  1. Error Handling: Use appropriate HTTP status codes and return meaningful error messages.
{
"error": {
"code": 404,
"message": "User not found"
}
}
  1. Security: Implement authentication (e.g., OAuth, JWT) and authorization mechanisms. Use HTTPS to encrypt data in transit. Validate and sanitize input to prevent security vulnerabilities like SQL injection or XSS.
  2. Documentation: Provide clear and comprehensive documentation for your API using tools like Swagger or Postman.
  3. Rate Limiting and Throttling: Implement rate limiting to protect your API from abuse and ensure fair usage.
  4. Caching: Use caching mechanisms to improve performance, especially for data that doesn’t change often.
  5. Use Pagination: Implement pagination for endpoints that return large datasets to improve performance and user experience.
  6. Consistent Responses: Ensure that API responses are consistent in format (usually JSON) and structure across different endpoints.
  7. Test Thoroughly: Write unit tests, integration tests, and end-to-end tests to ensure your API works correctly under different scenarios.

A well-designed REST API might look like this

GET /api/v1/users?page=2&limit=20        // Fetch 20 users from page 2
POST /api/v1/users // Create a new user
GET /api/v1/users/123 // Fetch a user with ID 123
PUT /api/v1/users/123 // Update user with ID 123
DELETE /api/v1/users/123 // Delete user with ID 123

How do you handle errors in GraphQL Provide examples of error handling​

In GraphQL, error handling is an important aspect of API design. Unlike REST, where HTTP status codes are typically used to indicate errors, GraphQL uses a different approach.

  • Handling Errors in Resolvers
    Throwing Errors
    In GraphQL, you can throw errors directly from the resolver function. These errors will be caught and included in the errors field of the GraphQL response.
const resolvers = {
Mutation: {
createUser: async (parent, args, context) => {
if (!args.input.email.includes('@')) {
throw new Error('Invalid email address');
}
return context.db.createUser(args.input);
},
},
};

Returning Partial Data with Errors
GraphQL allows returning partial data along with errors. This means that some fields can succeed while others fail.

{
"data": {
"user": null
},
"errors": [
{
"message": "User not found",
"locations": [{ "line": 2, "column": 3 }],
"path": ["user"]
}
]
}

Custom Error Handling
You can create custom error classes to handle different types of errors and include additional information like error codes.

class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.code = 404;
}
}

const resolvers = {
Query: {
user: async (parent, args, context) => {
const user = await context.db.getUserById(args.id);
if (!user) {
throw new NotFoundError('User not found');
}
return user;
},
},
};
  • Handling Validation Errors Input Validation
    You can validate input data in the resolver or using middleware. If the validation fails, throw an error.
const resolvers = {
Mutation: {
createUser: async (parent, args, context) => {
if (!args.input.email.includes('@')) {
throw new Error('Invalid email address');
}
return context.db.createUser(args.input);
},
},
};
  • Global Error Handling
    Using FormatError
    GraphQL allows you to customize the format of errors using the formatError function. This can be useful for hiding internal errors and returning more user-friendly messages.
const { ApolloServer, gql } = require('apollo-server');

const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (err) => {
if (err.message.startsWith('Database Error: ')) {
return new Error('Internal server error');
}
return err;
},
});

How do you implement authentication and authorization in GraphQL Provide examples of using JWT​

Implementing authentication and authorization in GraphQL often involves using JSON Web Tokens (JWT) for secure token-based authentication.

  • Authentication
    User Login: The user logs in by providing credentials (e.g., username and password). If the credentials are correct, the server generates a JWT and returns it to the client.
mutation {
login(username: "john_doe", password: "password123") {
token
}
}

Generating a JWT
Upon successful login, create a JWT using a secret key. Include claims such as user ID, roles, or expiration time.

const jwt = require('jsonwebtoken');
const SECRET_KEY = 'your_secret_key';

const resolvers = {
Mutation: {
login: async (parent, args, context) => {
const user = await context.db.getUser(args.username);
if (!user || user.password !== args.password) {
throw new Error('Invalid credentials');
}
const token = jwt.sign({ userId: user.id }, SECRET_KEY, { expiresIn: '1h' });
return { token };
},
},
};

Client-Side Storage
The client stores the JWT (usually in local storage or cookies) and includes it in the Authorization header of subsequent requests.

  • Authorization Middleware for Authorization
    Use middleware to extract the JWT from the request headers and verify it. This middleware runs before the resolver to check if the user is authenticated and authorized.
const { ApolloServer, AuthenticationError } = require("apollo-server");
const jwt = require("jsonwebtoken");

const context = ({ req }) => {
const token = req.headers.authorization || "";
if (token) {
try {
const user = jwt.verify(token, SECRET_KEY);
return { user };
} catch (err) {
throw new AuthenticationError("Invalid/Expired token");
}
}
throw new AuthenticationError("Authentication token must be provided");
};

const server = new ApolloServer({
typeDefs,
resolvers,
context,
});

Resolver-Level Authorization
Check the user’s roles or permissions within the resolver to determine if they have access to the resource.

const resolvers = {
Query: {
user: async (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('You must be logged in');
}
const user = await context.db.getUserById(args.id);
if (context.user.id !== user.id && context.user.role !== 'admin') {
throw new AuthenticationError('Not authorized');
}
return user;
},
},
};
  • Example Mutation with JWT Authentication
mutation {
login(username: "john_doe", password: "password123") {
token
}
}

query {
user(id: "1") {
name
email
}
}
  • Example JWT Verification
const context = ({ req }) => {
const token = req.headers.authorization || "";
if (token) {
try {
const user = jwt.verify(token, "your_secret_key");
return { user };
} catch (err) {
throw new AuthenticationError("Invalid/Expired token");
}
}
throw new AuthenticationError("Authentication token must be provided");
};

In this setup, the JWT is verified before the resolver runs, ensuring that only authenticated and authorized users can access the data.

What tools and libraries do you use for working with GraphQL (e.g., Apollo, Relay)​

There are several tools and libraries commonly used to work with GraphQL, each serving different purposes from client-side data fetching to server-side schema creation.

Client-Side Libraries

  • Apollo Client
    A popular and feature-rich GraphQL client for React, Angular, and other JavaScript frameworks. It handles data fetching, caching, and state management. Features: Query and mutation handling, local state management, caching, subscriptions for real-time data.
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";
import { gql } from "@apollo/client";

const client = new ApolloClient({
uri: "https://api.example.com/graphql",
cache: new InMemoryCache(),
});

const GET_USERS = gql`
query GetUsers {
users {
id
name
email
}
}
`;
  • Relay
    A JavaScript framework for building data-driven React applications. Developed by Facebook, Relay is tightly integrated with GraphQL and emphasizes efficiency, scalability, and consistency. Features: Declarative data fetching, fragment containers, automatic query batching.
import { graphql, useFragment } from "react-relay";

const fragmentSpec = graphql`
fragment UserFragment on User {
id
name
email
}
`;

function UserComponent(props) {
const data = useFragment(fragmentSpec, props.user);
return <div>{data.name}</div>;
}
  • urql
    A lightweight and customizable GraphQL client for React that offers a simple interface and modularity.
    Features: Flexible data fetching, hooks-based API, exchange system for customizing behavior.
import { createClient, Provider } from "urql";

const client = createClient({
url: "https://api.example.com/graphql",
});

function App() {
return (
<Provider value={client}>
<YourComponent />
</Provider>
);
}

Server-Side Libraries

  • Apollo Server A fully-featured GraphQL server that works with any Node.js HTTP server framework (e.g., Express, Koa).
    Features: Schema creation, resolvers, middleware integration, authentication, subscriptions.
const { ApolloServer, gql } = require("apollo-server");

const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
}

type Query {
users: [User]
}
`;

const resolvers = {
Query: {
users: () => [{ id: 1, name: "John Doe", email: "john@example.com" }],
},
};

const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
  • GraphQL.js The core JavaScript reference implementation for a GraphQL server. It provides a flexible and low-level API for building GraphQL schemas and executing queries.
const { graphql, buildSchema } = require("graphql");

const schema = buildSchema(`
type Query {
hello: String
}
`);

const root = {
hello: () => "Hello world!",
};

graphql(schema, "{ hello }", root).then((response) => {
console.log(response);
});
  • Prisma
    A next-generation ORM that works well with GraphQL. It generates a type-safe database client based on your schema and offers seamless integration with GraphQL. Features: Type-safe queries, schema migrations, database agnostic (supports MySQL, PostgreSQL, SQLite).
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

const resolvers = {
Query: {
users: async () => {
return await prisma.user.findMany();
},
},
};
  • Development and Testing Tools
    GraphQL Playground
    An interactive, in-browser GraphQL IDE that allows you to explore the GraphQL schema, write and test queries and mutations, and view documentation. Often integrated with servers like Apollo Server.

GraphiQL
A web-based IDE for exploring GraphQL APIs. It provides a graphical interface to write, test, and visualize queries and mutations. Can be integrated with any GraphQL server setup.

Postman
A popular API development environment that supports REST and GraphQL APIs. It allows you to create and test GraphQL queries and mutations with a user-friendly interface.

Insomnia
An alternative to Postman, offering a sleek interface for testing REST and GraphQL APIs. It includes features like environment management and query templates. These tools and libraries provide a comprehensive ecosystem for working with GraphQL, from building and deploying GraphQL servers to efficiently managing data on the client side.