Planning the Architecture Design of Full-Stack Web Apps
Crafting Robust and Scalable Solutions from Frontend to Backend
Building web applications in the 1990s was quite a complex process. It required extensive knowledge of the limited technologies available at the time. The complexity lied in understanding the limited number of tools available and being able to bend them in different and creative ways to accomplish what would have been considered in the past complex functionality (anything from state management, authenticaiton and so on).
In the modern day, web applications continue to require complexity in their design and development, but for different reasons. Web applications nowadays demand a much wider array of functionalities given the access to a wide array of tools. With that in mind, we’re going to be exploring how one might plan the architecture of a web (full-stack) application, including all the considerations related to the front-end, back-end, database and hosting options available and most commonly used.
We are going to assume that requirements planning has already been completed and the sole focus is the evaluation of different web technologies, that we’ll think of as tools, that we’ll utilize to implement the required functionalities.
We’ll consider the following steps in the planning of a web application. The order of these steps vary across different developers and teams, but what is consistent across different implementations is that fact that all modern web applications require the consideration of all the subsequent steps.
Steps
Defining Your Tech Stack
Planning the Project Structure
Defining the API Endpoints
Authentication and Authorization
Database Design
Client State Management
Deployment
1. Defining Your Tech Stack
A tech stack refers to the languages, frameworks, libraries and other technologies that will be adopted for a given project. Typically we start with core technologies that drive the core development of our application and we incrementally adopt smaller libraries and packages that help us achieve particular features by abstracting away those concerns and relying on the community support and maintenance of said packages.
Although software continues to be a disruptive force, most modern web app technologies have begun to establish mass adoption, community support, and stability in the tools that are frequently selected.
For example, developing web apps today would require the use of a front-end framework that would allow you to create dynamic applications, personalized to every user. This has been accomplished primarily through the use of Javascript and the document object model (DOM), an API that allows you to interact directly with the user’s browser (UI) through a set of objects and functions. React or Vue as the most common frameworks adopted today, given their robustness, feature-sets, stability, and community support. Styling decisions are then made based on which one of these you select.
Frontend
Frameworks: React, Angular, Vue.js are popular choices
Styling: CSS, Sass, or CSS-in-JS solutions like styled-components
In the backend, there is a tremendous amount of flexibility since an API is nothing more than a server, and a server is nothing more than a live process with a wrapper that includes communication capabilities through the HTTP/S protocol.
Despite the amount of choice developers have here, most of them continue to adopt either Node, Python or Java (primarily) since they’re languages that are easy to learn, offer many opportunities for jobs, and cover all the basic capabilities for what an API/server might entail. A language, however, is not an API, and what we’re actually looking for are also frameworks written in these languages which come with the features needed to convert files in one of these languages into a functioning web server.
Backend
Languages: Node.js, Python, Ruby, Java
Frameworks: Express (Node.js), Django (Python), Ruby on Rails
Finally, most web apps and APIs will require the need to store and retrieve data specific to users, which is where databases come in.
Given that databases date back to the 1970, there is much more stability in the databases available. MySQL is the most common and widely used, with Postgres being a very close alternative. Postgres provides additional functionality that in many times can be useful.
It is recommended to use MySQL if you’re building your web app with a more long-term vision in mind for clean software development, or Postgres when you are wanting more flexibility and enhanced capabilities for more agile development intially.
Database
SQL: PostgreSQL, MySQL
NoSQL: MongoDB, CouchDB
Example Web App
We’re going so witness the simplest example of a web app, with its different components defining our initial state.
Front-End
On the front-end, we’ve selected React. What you see below is a single component that is rendered directly to the DOM. In the subsequent sections we’ll begin establishing a project structure to allow for multiple pages, components, state management, and API communication.
// Frontend: React
import React from 'react';
import ReactDOM from 'react-dom';
function App() {
return <h1>Hello, world!</h1>;
}
ReactDOM.render(<App />, document.getElementById('root'));
Back-End
On the back-end, we’ve selected Node and Express as the framework, given that it is also written in Javascript. Below you see the simplest way to define our initial state of a web server and API, with a single endpoint and a live server listening on port 3000. When we deploy this back-end API/app, all that we’ll be doing is running this process on a remote server somewhere so that our client app can find it and communicate with it.
// Backend: Express.js
const express = require('express');
const app = express();
app.get('/api', (req, res) => {
res.send('Hello from the backend!');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
2. Planning the Project Structure
Project structure does not
Organize your project so it’s easy to navigate and maintain:
Frontend
src/: Contains all source code
components/: Reusable UI components
pages/: Different views or routes
services/: API calls and other services
Backend
routes/: Define API endpoints
controllers/: Handle the business logic
models/: Define database schemas
middlewares/: Custom middleware functions
Example:
super-cool-app/
├── api/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ └── app.js
├── client/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/
│ │ └── App.js
└── package.json
3. Defining the API Endpoints
APIs are the glue between your frontend and backend
Plan your endpoints carefully based on the set of actions that will be required by the applications that utilize your APIs
Often, the users of your API can guide the actions that you will implement
Also often, if you atomically specify the entities your your REST API resources, you can predictably create a minimal set of endpoints that you’ll most likely have to implement (like below, where we do a GET/POST/PUT/DELETE)
RESTful API
GET /users: Fetch users
POST /users: Create a new user
PUT /users/:id: Update a user
DELETE /users/:id: Delete a user
Example:
// routes/users.js
const express = require('express');
const router = express.Router();
const usersController = require('../controllers/users');
router.get('/', usersController.getUsers);
router.post('/', usersController.createUser);
router.put('/:id', usersController.updateUser);
router.delete('/:id', usersController.deleteUser);
module.exports = router;
// controllers/users.js
const getUsers = (req, res) => {
// Fetch users from database
res.send('Get users');
};
const createUser = (req, res) => {
// Create a new user
res.send('Create user');
};
const updateUser = (req, res) => {
// Update a user
res.send('Update user');
};
const deleteUser = (req, res) => {
// Delete a user
res.send('Delete user');
};
module.exports = { getUsers, createUser, updateUser, deleteUser };
Architecture Design for APIs
Designing APIs isn't just about writing endpoints. It’s about creating a robust structure that makes your API scalable, maintainable, and easy to use. Let’s go through the key aspects of API architecture design, from planning to implementation. 1. Define the Purpose and Requirements
Best Practices for REST APIs
Understanding and applying best practices in the design and development of RESTful APIs can make a world of difference in their usability and maintenance. This post covers the essential best practices to API development with practical examples and code snippets to guide you through key concepts.
4. Authentication and Authorization
If you simply deployed your API at this point, it would be completely public and exposed to any user that had the URL
Although the endpoints and their payload structure is not public, it is easy nowadays to reverse engineering public APIs, which is why it is essential to implement authentication and authorization modules as a form of security - but, also, as an additional feature allowing your application to support functionality based on user groups and their permissions
There are different types of authentication and authorization mechanisms that are common to implement:
User Authentication
This includes authentication for login and registration
This is typicalyl achieved with a database and users table that stores encrypted passwords and an authentication protocol or library, like JWT (JSON Web Tokens) or OAuth, for secure, token-based authentication
Role-Based Access Control (RBAC)
This defines user roles and permissions, which restricts access to certain parts of the application based on user roles
Commonly implemented as attributes on the users database table, set for each independent user
Session Management
Ensure secure session handling to prevent unauthorized access and session hijacking
Often achieved by using libraries like Express-Session or frameworks like Django’s built-in session management
Example:
// Authentication middleware
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const token = req.header('Authorization').replace('Bearer ', '');
if (!token) {
return res.status(401).send('Access denied');
}
try {
const decoded = jwt.verify(token, 'your_jwt_secret');
req.user = decoded;
next();
} catch (ex) {
res.status(400).send('Invalid token');
}
};
module.exports = authenticate;
5. Database Design
Design your database schema to reflect the data relationships.
Common steps involved:
Schema Design
Plan your database schema to ensure it supports all necessary data relationships and efficiently handles queries
Use tools like ER diagrams for visualization
Normalization
Apply normalization techniques on your database design to reduce redundancy and improve data integrity (before actual implementation)
Execute normalization techniques only up to an appropriate level depending on performance considerations
Indexing
Implement indexing to speed up query performance on frequently searched fields
However, be cautious of over-indexing which can degrade write performance
Backup and Recovery
Establish a reliable backup and recovery strategy to protect against data loss, ensuring regular backups and tested recovery procedures
SQL Example:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100) UNIQUE,
password VARCHAR(100)
);
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id),
title VARCHAR(100),
body TEXT
);
NoSQL Example:
// MongoDB schema
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: String,
email: { type: String, unique: true },
password: String
});
const postSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
title: String,
body: String
});
const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
6. Handling State Management
For complex apps, managing state efficiently is crucial.
When we refer to state, there are actually different states that we are talking about, and we must be more specific in our language, based on the location of where we are specifically talking about.
On the client (front-end) side, there are the following states:
Local State
Local state refers to component-level state
For something like React, this involves using the
useState
hook
Global State
React also has the Context API to be able to share state between components, something that is achieved by actually writing the state separately (outside of the components)
There are also state management libraries like Redux, MobX that handle application-wide state, ensuring a single source of truth
On the server side, there are the following states:
Server-Side State
Any sort of caching mechanism or memory database, like Redisor Memcached which serve as short-term state/storage blocks - primarily utilized to prevent hits to the database, and particularly useful when we have APIs that have thousands to millions of hits per second
Persistence
Persisting state across sessions refers to databases - where long-term storage always sits
Example:
// Redux example
import { createStore } from 'redux';
const initialState = { count: 0 };
function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
default:
return state;
}
}
const store = createStore(reducer);
store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // { count: 1 }
7. Deployment
Finally, the topic of deployment cannot be escaped in the planning of complex web apps, especially due to the increasing number of options for where you are able to deploy an application.
Deployment also goes beyond deploying apps and also commonly constitutes the following tasks:
Setting up servers and configuration
Creating and maintaining multiple environments for development, testing, and production versions
Continuous monitoring, alerts and reporting
Continuous Integration/Continuous Deployment (CI/CD)
This includes setting up CI/CD pipelines with tools like Jenkins, GitHub Actions, or GitLab CI to automate testing, building, and deployment processes
Hosting Platforms
This involves experimenting with, testing and selecting one or more hosting platforms that fits your needs
Common choices today include AWS, Azure, Google Cloud,
More specialized services are also common for smaller apps, like Vercel or Netlify for frontend or Heroku or Digital Ocean for backend
Containerization
Docker is a very powerful tool for creating consistent and portable environments
Kubernetes in an enhanced version of Docker that comes with additional functionality, and used primarily to orchestrate Docker deployments with for robuyst scalability and reliability
Monitoring and Logging
One of the final steps involves the implementation of monitoring and logging software to track application performance and identify issues in real-time
Popular and excellent tools that address these tasks are Prometheus, Grafana, and ELK Stack (Elasticsearch, Logstash, Kibana)