An approach for conveniently and quickly building modular, scalable frontend applications of any size.
ย
The examples use React + TypeScript, BUT you can use absolutely anything: any language, platform, or point in the space-time continuum.
WSD naturally mirrors any system that can be described as: โA tree-like UI that can be manipulated with functions and can work with variablesโ โ yes, in exactly that crude wording. That is why it has worked, works now, and will keep working.
From the author
ย
Hello, dear reader. My name is David Shekunts, and I am a Tech Lead with 12 years of experience.
WSD is one of the best discoveries of my programming career. It maps so naturally onto how frontend is structured that it fits almost any task.
I have been using this approach for about 7 years, and I have never had a case where it created problems or failed me.
Does this sound like magic / a silver bullet? I may be a fool, but damn it, I will say โyes.โ Surprisingly, I have not yet found a case where this structure does not work.
And now, dear reader, I challenge you to a duel: find such a case, show it to me, and do the most important thing in human history โ destroy the first and last silver bullet.
Subscribe on X channel [ $davids.sh ] , where announcements of new chapters and updates to existing ones will be posted.
Example applications
Applications written using WSD:
ย
(coming soon)
ย
Glossary
ย
Entityโ a description of some data. There are 3 types:UIโ data of a specific UI component: whether it is visible, what is written in an input, etc. This is often 80% of all code.Utilโ geometry (Pointin space,Triangle), an external API (Stripe), and everything else predefined by the external world.Businessโ the state of business entities your application works with. Very often, it comes from the backend and later goes back into the backend database.
ย
// Entities are described through some kind of typing, whether type, interface, or class // Example of a UI Entity type LoginPopup = { closed: boolean collapsed: boolean loading: boolean error: string } // Example of a Util Entity: Triangle type Point = { x: number; y: number; } type Triangle = { one: Point; two: Point; three: Point; } // Example of a Util Entity: Stripe API type StripeCreateInvoceRequest = { id: string total: number } type StripeCreateInvoceResponse = { success: boolean message: string } // Example of a Business Entity type Product = { name: string price: number comments: Comment[] } type Cart = { products: Product[] totalSum: number }
ย
Stateโ a particular combination of Entities in a particular Component at a specific moment in time.
ย
// loginPopup is already State based on the LoginPopup UI entity const loginPopup = useState<LoginPopup>({ ... }) // State of util code for a triangle const pointOne = useState<Point>({ x: 10, y: 10 }) // ... const triangle = useState<Triangle>({ one: pointOne, ... }) // State of the Cart business entity const cart = useState<Cart>({ ... })
ย
Logicโ a set of functions that do something with State. Usually there are 3 categories:UIโ logic for the state of a specific UI component: whether it is visible, what is written in an input, etc. This is often 80% of all code.Utilโ for example, functions for converting centimeters to meters, calculating a hypotenuse through cosine, converting dollars to rubles, and everything else predefined by the external world: physics, laws, mathematics, etc.Businessโ functions that work with the Business Entities of your specific application (Put Product into Cart,Delete Comment). This often involves working with the backend.
ย
// UI logic โ works with interface state const openLoginPopup = (popup: LoginPopup): LoginPopup => ({ ...popup, closed: false, }) const closeLoginPopup = (popup: LoginPopup): LoginPopup => ({ ...popup, closed: true, error: "", }) const setLoginError = ( popup: LoginPopup, error: string, ): LoginPopup => ({ ...popup, error, loading: false, }) // Util logic โ knows nothing about your application const getDistance = (a: Point, b: Point): number => { return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2) } const getTrianglePerimeter = (triangle: Triangle): number => { return ( getDistance(triangle.one, triangle.two) + getDistance(triangle.two, triangle.three) + getDistance(triangle.three, triangle.one) ) } // Business logic โ works with the application's business entities const getCartTotalSum = (cart: Cart): number => { return cart.products.reduce((sum, product) => { return sum + product.price }, 0) } const addProductToCart = ( cart: Cart, product: Product, ): Cart => { const products = [...cart.products, product] return { ...cart, products, totalSum: getCartTotalSum({ ...cart, products }), } } const removeProductFromCart = ( cart: Cart, productName: string, ): Cart => { const products = cart.products.filter(product => { return product.name !== productName }) return { ...cart, products, totalSum: getCartTotalSum({ ...cart, products }), } }
ย
Componentโ a UI component with any variant or combination of logic:
type CartWidgetProps = { user: User | null cart: Cart openAuthPopup: () => void } const CartWidget = ({ user, cart, openAuthPopup, }: CartWidgetProps) => { const [loading, setLoading] = useState(false) const [error, setError] = useState("") const pay = async () => { if (!user) { openAuthPopup() return } setLoading(true) setError("") const response = await createStripeInvoice({ id: user.id, total: cart.totalSum, }) setLoading(false) if (!response.success) { setError(response.message) return } window.location.href = response.paymentUrl } return ( <div> <div>Products: {cart.products.length}</div> <div>Total: {cart.totalSum}</div> {error && <div>{error}</div>} <button disabled={loading} onClick={pay}> {loading ? "Loading..." : "Pay"} </button> </div> ) }
ย
Splitting the application into 5 parts
ย
pagesโ Components with State and Business Logic that are opened through some URL. You could also say: โawidgetfor a URL.โ
widgetsโ Components with State and Business Logic that are reused across differentpages.
appsโ allow us to assemble different applications from different sets ofpages, for example for different URLs, different roles, or different environments such as web and Electron.js.
libsโ code as libraries that could just as well live in npm, but you have reasons to keep them locally.
ย
Structuring rules
This is where a very important concept appears: โLevel.โ
ย
One of the huge frontend problems is how to structure code so that reusable code is conveniently available and understandable where it is reused, while not being present where it is not needed.
ย
You could phrase it like this: โI want to be able to delete 1 folder and completely destroy an entire feature with itโ โ or: โI want to move 1 folder from one project to another and get fully working functionality.โ
ย
To implement this modularity, we need Levels โ application structuring rules that define what can interact with what.
ย
src/ โโโ apps/ # application assembly points โ โโโ shop/ โ โโโ index.tsx โ โโโ pages/ # pages bound to URLs โ โโโ home/ โ โ โโโ index.tsx โ โโโ cart/ โ โโโ index.tsx โ โโโ widgets/ # globally reusable widgets โ โโโ auth-widget/ โ โ โโโ index.tsx โ โโโ cart-widget/ โ โโโ index.tsx โ โโโ libs/ # local libraries โโโ ui-kit/ โ โโโ index.ts โโโ auth-sdk/ โ โโโ index.ts โโโ math-fns/ โโโ index.ts
Here are the rules that define how Levels interact:
ย
# This may look complicated, but it becomes intuitive very quickly. # Literally look at how folders are nested inside each other โ look at the indentation, # and everything becomes clear. src/ โโโ apps/ # not considered, because apps cannot be reused; this is the highest point of the entire hierarchy โ โโโ libs-1/ # level 0 (root) โ โโโ widgets/ โ โโโ widget-1/ # level 0 (root), parent of widget-2 โ โโโ libs-2/ # level 1, child of widget-1 โ โโโ widget-2/ # level 1, child of widget-1 โ โโโ pages/ โโโ page-1/ # level 0 (root), parent of widget-3 and page-2 โโโ libs-3/ # level 1, child of page-1, sibling of page-2, cousin of widget-2 and libs-2 โโโ widget-3/ # level 1, child of page-1, sibling of page-2, cousin of widget-2 and libs-2 โโโ pages/ โโโ page-2/ # level 1, child of page-1, sibling of widget-3 and libs-3, cousin of widget-2 and libs-2, parent of widget-4 โโโ widget-4/ # level 2, child of page-2
ย
- Level X is the folder of a specific
widgets/pages/libs, and each nested folder increases X by 1. - Child โ nested
widgets/pages/libs. - Parent โ the folder that contains the current one.
- Sibling โ folders located directly next to the current one.
- Cousin โ a folder located in another
widgets/pages/libsbranch at the same nesting level as the current one. The same rules apply to its descendants.
- Root Level (level 0) is where the highest-level
apps,pages,widgets, andlibslive.
- What can be placed where:
libscan be placed inside any folder.widgetscan be placed inside any folder, even inside otherwidgets.pagescan be placed only insidepages.appscan exist only at the root level.
- Reuse rules:
- Nothing can reuse Cousins โ if something is in a separate branch, you cannot touch it.
- Everything may use parent, child, and sibling
widgets. - Everything may use parent and sibling
libs. libsmay not use anything except otherlibsfrom parent levels.pagesmay use onlypagesnested inside them, not parent pages and not sibling pages.
Additional notes
ย
- In practice, a
widget/pagemay consist of just anindex.tsfile that contains both Logic and UI.
- If you have only 1 app, you can simply create
index.tsin the root ofsrcand avoid creating anappsfolder.
- If you want to create a
widgetthat has no Component, then it should be created as a library inlibs.
- Do not create folders like
widget/pageif you do not use them.
ย
Private namespace
If you were publishing a library, for example with API types or an entire SDK for your system, how would it live in npm? Most likely as
@${company}/some-sdk.ย
As I said above,
libs is literally npm. Therefore, inside libs, you should create an @${company} folder and add libraries that are specifically about your application there.ย
src/ โโโ libs/ โโโ @company/ # private project libraries โโโ ui-kit/ # design system โ โโโ index.ts โโโ backend-sdk/ # backend API client โ โโโ index.ts โโโ analytics-sdk/ # analytics client โโโ index.ts
ย
Without Levels
If Levels are unclear to you, or if you are not yet sure what should live where, then everything is very simple: put Widgets into the top-level
widgets, Pages into the top-level pages, and Libraries directly into libs.ย
# Put everything right here: src/ โโโ apps/ โ โโโ app-1/ โ โโโ app-2/ โ โโโ pages/ โ โโโ page-1/ โ โโโ page-2/ โ โโโ widgets/ โ โโโ widget-1/ โ โโโ widget-2/ โ โโโ libs/ โโโ lib-1/ โโโ lib-2/
ย
Additional
tests
Tests can also live at any level. The important thing is that they live next to the thing they test. Again, we are trying to โmove all related code by moving a folder,โ so tests must live next to the code they cover.
ย
src/ โโโ apps/ โ โโโ shop/ โ โโโ index.tsx โ โโโ tests.ts # application assembly test โ โโโ tests/ โ โโโ auth-flow.ts # global e2e test โ โโโ libs/ โ โโโ @company/ โ โโโ backend-sdk/ โ โโโ index.ts โ โโโ tests.ts # SDK test โ โโโ widgets/ โ โโโ auth-widget/ โ โโโ index.tsx โ โโโ tests.tsx # widget test โ โโโ pages/ โโโ home/ โโโ index.tsx โโโ tests.tsx # page test โโโ libs/ โ โโโ home-api/ โ โโโ index.ts โโโ widgets/ โโโ cart-widget/ โโโ index.tsx โโโ tests.tsx # local widget test
Final structure
A hypothetical online store:
ย
src/ โโโ apps/ โ โโโ shop/ # shop application โ โ โโโ index.tsx โ โโโ admin-panel/ # admin panel application โ โโโ index.tsx โ โโโ tests/ โ โโโ auth-flow.ts # global auth e2e test โ โโโ libs/ โ โโโ @company/ โ โ โโโ backend-sdk/ # backend API SDK โ โ โโโ index.ts โ โ โโโ tests.ts โ โโโ stripe/ # local wrapper around Stripe โ โโโ index.ts โ โโโ widgets/ โ โโโ auth-widget/ # global authentication widget โ โโโ index.tsx โ โโโ tests.tsx โ โโโ pages/ โโโ home/ # / page โโโ index.tsx โโโ tests.tsx โโโ libs/ โ โโโ home-api/ โ โโโ index.ts โโโ widgets/ โ โโโ cart-widget/ # local cart widget โ โโโ index.tsx โ โโโ tests.tsx โโโ pages/ โโโ product/ # /product/:productId page โโโ index.tsx โโโ widgets/ โโโ product-description/ โโโ index.tsx
ย
Related concepts
ย
- FALSe โ an application structure that describes a similar approach, but for backend applications.
- Feature-Sliced Design โ also tries to structure frontend by semantic layers and slices, but WSD is simpler: fewer terms, fewer levels, and less room for interpretation. I would not even recommend FSD, because everyone interprets it differently, and loose interpretations always prove that it is too complex and not truly โunderstandable.โ
- Vertical Slice Architecture โ the idea of keeping all code for one feature nearby: UI, state, logic, and tests. It is similar to the WSD rule: โmove the folder โ move the feature.โ
- Colocation โ the principle of keeping related code nearby: component, state, tests, styles, local utils.
- Locality of Behavior โ logic should live near the place where it is actually used, not in some abstract global folder.
- Clean Architecture / Hexagonal Architecture โ useful as an analogy for
libsand business logic, but often too heavy for frontend.
- Package by Feature โ structure by user/business capabilities rather than by technical file types.
ย
Subscribe on X channel [ $davids.sh ] , where announcements of new chapters and updates to existing ones will be posted.