Full-stack Flashcards
What happens when you type a URL into the browser and hit enter?
-
DNS Resolution: The browser checks the domain name (e.g.,
www.example.com
) and queries a DNS server to translate it into an IP address (the location of the web server). - HTTP Request: The browser sends an HTTP request to the web server at the resolved IP address, requesting the specific resource (e.g., a webpage).
- Server Response: The server processes the request, retrieves the resource (like an HTML file), and sends it back as an HTTP response.
- Rendering: The browser parses the HTML, executes any JavaScript, and renders the page. It may send additional requests for assets like CSS, images, or scripts until the page is fully loaded.
What’s the difference between monolithic and microservice architectures?
Monolithic architecture is a software design where all components of an application are tightly integrated and deployed as a single unit. It’s easier to develop initially but can become complex and harder to scale or maintain as the application grows.
Microservice architecture breaks an application into smaller, independent services that each handle a specific function. These services communicate over a network, allowing for better scalability, flexibility, and easier maintenance, though it introduces complexity in terms of communication and deployment.
The key difference is that monolithic is a single, unified system, while microservices are separate, self-contained services working together.
How do you design a RESTful API, and what makes it RESTful?
To design a RESTful API, ensure it is resource-based, where each resource is represented by a URI, and use standard HTTP methods like GET, POST, PUT, and DELETE to perform operations on those resources. The API should be stateless, meaning each request must contain all the information needed to process it.
Additionally, you should use appropriate HTTP status codes to communicate the result of the request. A RESTful API adheres to these principles to be simple, scalable, and easy to maintain.
When would you use GraphQL over REST?
You would use GraphQL over REST when you need more flexibility in querying data. With GraphQL, clients can request exactly the data they need, reducing over-fetching or under-fetching issues that are common in REST APIs.
It’s also useful when dealing with complex or nested data structures, as it allows you to fetch multiple related resources in a single request.
Additionally, GraphQL can handle real-time data updates with subscriptions, making it a better choice for dynamic applications.
What are CORS and how do you handle CORS issues?
CORS (Cross-Origin Resource Sharing) is a security feature implemented by browsers to prevent malicious websites from making requests to a different domain than the one that served the web page. It determines whether a web page can make requests to a different domain, protocol, or port.
To handle CORS issues, the server must include the appropriate CORS headers in the response, such as:
- Access-Control-Allow-Origin: Specifies which domains are allowed to access the resource.
- Access-Control-Allow-Methods: Defines the allowed HTTP methods (GET, POST, etc.).
- Access-Control-Allow-Headers: Lists the headers that can be sent with the request.
For development purposes, you can also use middleware like CORS in Express.js to automatically handle these headers.
How do you manage authentication state on the frontend securely?
To manage authentication state securely on the frontend, you can store tokens (e.g., JWT) in HTTP-only cookies to prevent access via JavaScript, reducing the risk of XSS attacks.
Use localStorage or sessionStorage cautiously, as they can be vulnerable to XSS attacks.
Implement token expiration and refresh mechanisms to ensure session security, and always use HTTPS to protect data in transit.
Additionally, avoid exposing sensitive data in the frontend and rely on backend verification for authentication.
What are the pros and cons of server-side rendering (SSR) vs client-side rendering (CSR)?
Server-Side Rendering (SSR) has the following pros and cons:
- Pros: Faster initial page load since the HTML is pre-rendered on the server, improving SEO because search engines can index the content easily. It also benefits users with slow connections or devices, as the server sends a fully-rendered page.
- Cons: Can put more load on the server and increase response times, especially under heavy traffic. It may also lead to slower subsequent interactions since the client still needs to fetch and re-render data dynamically.
Client-Side Rendering (CSR) has its own advantages and disadvantages:
- Pros: Faster interactions after the initial load, as subsequent navigation only requires fetching data. It reduces server load and allows for more dynamic, interactive UIs.
- Cons: Slower initial load since the client must download, parse, and execute JavaScript. SEO can be challenging unless additional measures (like prerendering) are taken, as search engines may have difficulty indexing content rendered only on the client side.
How would you implement lazy loading in a single-page application (SPA)?
To implement lazy loading in an SPA, you can use code splitting tools like Webpack or React.lazy() to load components only when they are needed, rather than all at once.
For example, in React, you can dynamically import components with React.lazy() and wrap them in a Suspense component to handle loading states. Route-based lazy loading can also be used to load specific components when users navigate to different parts of the app.
This approach reduces initial load time and improves performance by only loading necessary code.
How do you handle environment variables in a frontend app securely?
To handle environment variables securely in a frontend app, avoid directly exposing sensitive information like API keys in the client-side code. Use build tools (e.g., Webpack or Vite) to inject environment variables into the build process, but ensure that only non-sensitive information (like URLs) is exposed to the client-side.
Sensitive data should be stored securely on the server, and API calls should be made through a backend that acts as a proxy to keep secrets hidden from the frontend.
Additionally, use HTTPS to encrypt communications between the client and server.
How do you optimize frontend performance for time to first byte (TTFB) and page speed?
To optimize frontend performance for Time to First Byte (TTFB) and page speed, focus on reducing server response times and optimizing resource loading:
- Improve server performance: Use techniques like caching (e.g., CDNs, HTTP caching headers) and optimizing server-side processing to reduce TTFB. Consider using server-side rendering (SSR) to send a pre-rendered HTML page quickly.
- Minify and compress resources: Minify CSS, JavaScript, and HTML files, and enable gzip or Brotli compression to reduce the size of assets sent over the network.
- Lazy loading and code splitting: Implement lazy loading for images and other assets, and code splitting for JavaScript to load only the necessary parts of the app initially.
-
Optimize images and assets: Use modern image formats like WebP, compress images, and implement responsive images (
srcset
) to ensure assets load quickly without sacrificing quality.
These techniques help reduce both TTFB and overall page load times, enhancing user experience.
How does event-driven architecture work in Node.js?
In Node.js, event-driven architecture is based on the Event Loop, which listens for and handles events asynchronously. Events are emitted by EventEmitter objects, and corresponding event listeners (callbacks) are triggered when an event occurs.
This architecture allows Node.js to efficiently handle I/O operations, like file reads or HTTP requests, without blocking the main thread.
As a result, Node.js is well-suited for real-time applications, capable of handling high concurrency with minimal resource usage.
What are the differences between synchronous and asynchronous programming on the backend?
In synchronous programming, each operation is executed sequentially, meaning the server waits for one task to complete before moving on to the next. This can lead to slower performance when handling multiple requests, as the server is blocked during I/O operations like database queries or file reads.
In asynchronous programming, operations are executed non-blocking, allowing the server to continue processing other requests while waiting for time-consuming tasks to finish. This approach improves performance and scalability, as the server can handle many tasks concurrently without being blocked, making it ideal for handling I/O-heavy operations like in Node.js.
How do you handle error logging and monitoring in a backend service?
To handle error logging and monitoring in a backend service, you can use structured logging tools like Winston or Bunyan to capture detailed error logs with relevant metadata (e.g., timestamps, request IDs). Integrating a centralized logging system like ELK Stack (Elasticsearch, Logstash, Kibana) or Graylog helps aggregate logs from multiple services for better analysis.
For monitoring, tools like Prometheus or Datadog can be used to track metrics, alert on failures, and monitor system health. Additionally, setting up automatic error reporting with services like Sentry can provide real-time alerts and detailed error tracking, allowing you to address issues quickly.
What are middleware functions in Express, and how do they work?
Middleware functions in Express are functions that have access to the request, response, and the next middleware function in the request-response cycle. They are used to process incoming requests before they reach the route handler or to modify the response before sending it back to the client.
Middleware can perform tasks such as logging requests, parsing request bodies, handling authentication, or managing error handling. To use a middleware, you define it and call the next() function to pass control to the next middleware or route handler. You can apply middleware globally or to specific routes.
How would you implement rate limiting in an API?
To implement rate limiting in an API, you can use middleware to restrict the number of requests a user can make within a specific time frame.
Libraries like express-rate-limit in Express.js help set limits on requests per IP, for example, allowing 100 requests per hour.
For scalable applications, you can use Redis to store and track request counts across multiple instances, enabling distributed rate limiting. This approach helps prevent abuse, reduces server load, and ensures fair access to the API.
When should you use SQL vs NoSQL databases?
You should use SQL databases when your data is structured, relational, and requires complex queries, joins, or transactions. SQL databases are ideal for applications where data integrity, ACID (Atomicity, Consistency, Isolation, Durability) properties, and consistency are critical, such as in banking systems or e-commerce platforms.
NoSQL databases are better suited for applications with unstructured or semi-structured data, where flexibility and scalability are needed. They are ideal when dealing with large volumes of rapidly changing data, such as in social media platforms, content management systems, or real-time analytics, where high availability and horizontal scaling are more important than strict consistency.
How would you model a many-to-many relationship in both SQL and NoSQL?
In SQL, a many-to-many relationship is modeled using a junction table, which contains foreign keys referencing the primary keys of the two related tables.
For example, in a students and courses relationship, a student_courses table would link student_id and course_id as foreign keys. In NoSQL, such as MongoDB, you typically store references to related documents as arrays within a document, like having an array of course IDs in the student document.
Alternatively, documents can be embedded if the data is small and non-complex. Graph databases like Neo4j handle many-to-many relationships with nodes and edges, making it easy to traverse complex relationships between entities.
What is indexing, and how does it impact query performance?
Indexing is a technique used to optimize the speed of data retrieval operations on a database. An index is a data structure that stores a sorted list of values from one or more columns of a table, along with pointers to the corresponding rows in the database. By using indexes, databases can quickly locate data without scanning the entire table, which greatly improves the performance of SELECT queries, especially on large datasets.
However, indexing also has trade-offs: while it speeds up read operations, it can slow down write operations (INSERT, UPDATE, DELETE) because the index must be updated whenever the data changes. Additionally, indexes consume extra disk space. Therefore, careful planning is needed to choose the right columns to index based on query patterns.
How would you prevent SQL injection or NoSQL injection attacks?
To prevent SQL injection in relational databases, always use parameterized queries or prepared statements instead of concatenating user input directly into SQL queries. For example, in languages like JavaScript or Python, use libraries that support parameterized queries (e.g., pg for PostgreSQL or mysql2 for MySQL) to safely handle user input. This ensures that input is treated as data, not executable code.
For NoSQL injection (commonly in document-based databases like MongoDB), ensure that queries are constructed safely by avoiding direct user input in query objects. Use validation and sanitization techniques to filter and escape user input, and apply proper access controls to limit which queries users can execute. Additionally, avoid using eval() or similar functions, which could execute arbitrary JavaScript code. Properly validating and sanitizing input for both types of injection attacks is crucial to application security.
How do transactions work in relational databases?
In relational databases, a transaction is a sequence of operations that are executed as a single unit, ensuring data integrity and consistency. It follows the ACID properties: Atomicity ensures all operations succeed or fail together, Consistency guarantees the database remains in a valid state, Isolation prevents interference between concurrent transactions, and Durability ensures changes are permanent once committed. Transactions are controlled with commands like BEGIN TRANSACTION, COMMIT, and ROLLBACK to manage their execution.
This ensures that the database maintains its integrity, even in the case of errors or system failures.
What’s the difference between session-based and token-based authentication?
Session-based authentication stores user authentication information on the server side. After the user logs in, the server creates a session, stores session data (such as user information), and sends a session ID to the client in a cookie. Each subsequent request includes the session ID, allowing the server to authenticate the user by checking the session data stored on the server.
Token-based authentication, on the other hand, uses tokens (commonly JWT) that contain user information, which are signed and sent to the client. The client stores the token (usually in localStorage or a cookie) and includes it in the Authorization header for subsequent requests. The server doesn’t store any session data but verifies the token’s validity and authenticity using a secret key.
The key difference is that session-based relies on server-side storage, while token-based is stateless and stores the user data directly within the token, making it more scalable for distributed systems.
How do JWTs work and how do you securely store and validate them?
JWTs (JSON Web Tokens) are compact tokens that securely transmit information between parties in three parts: a header (specifying the signing algorithm), a payload (containing the claims or data), and a signature (created by signing the header and payload with a secret key or private key). The server generates the token after successful authentication and sends it to the client, which stores it (typically in HTTP-only cookies or localStorage).
To securely store JWTs, it’s best to use HTTP-only cookies to mitigate XSS attacks, though localStorage can be used with caution. On each request, the server validates the token by checking its signature and expiration time, ensuring it hasn’t been tampered with.
Using HTTPS for secure transmission and securely managing the secret key is crucial for preventing unauthorized access or interception.
What are common attack vectors in full-stack apps (XSS, CSRF, SSRF), and how do you defend against them?
Common attack vectors in full-stack apps include XSS (Cross-Site Scripting), CSRF (Cross-Site Request Forgery), and SSRF (Server-Side Request Forgery). Here’s how to defend against them:
-
XSS (Cross-Site Scripting): XSS occurs when an attacker injects malicious scripts into web pages, which are then executed in the browser. To defend against XSS, always sanitize and escape user inputs, use Content Security Policy (CSP), and avoid using functions like
eval()
orinnerHTML
. Additionally, ensure that any data rendered to the page is properly escaped to prevent scripts from running. - CSRF (Cross-Site Request Forgery): CSRF tricks users into performing actions they didn’t intend, such as changing account settings. To protect against CSRF, use anti-CSRF tokens in forms and HTTP requests, ensuring that each request is validated for a unique token that cannot be forged. Also, ensure that sensitive requests are only made using HTTP POST and SameSite cookies to restrict cross-origin requests.
- SSRF (Server-Side Request Forgery): SSRF occurs when an attacker sends a request from the server to an internal or external resource, bypassing security controls. To mitigate SSRF, always validate and sanitize user inputs, particularly URLs and IP addresses, and restrict the server from making requests to internal resources unless necessary. Implementing network-level firewalls and whitelisting allowed destinations can also prevent unauthorized access.
By employing these defenses, you can significantly reduce the risk of these common security threats in full-stack applications.
How would you implement OAuth2 in a web app?
To implement OAuth2 in a web app, start by registering your app with an OAuth2 provider to obtain a client ID and client secret. Redirect the user to the provider’s authorization endpoint, where they can authenticate and grant permission, and then handle the redirect to obtain an authorization code.
Exchange the authorization code for an access token by sending a request to the provider’s token endpoint, using the client credentials.
Store the access token securely and use it to make authenticated requests on behalf of the user, handling token expiration with refresh tokens if necessary.