Choosing the Right Tech Stack for a New Web Project
Every new project starts with the same question, and it is almost always asked too early: what should we build it in? We get it — the stack feels like the first real decision. But in our experience, teams that pick a stack first and define the problem second end up rewriting both. Here is how we approach it at TCCB Solutions.
Start with constraints, not frameworks
Before we name a single library, we write down the things that cannot change. These are the constraints that actually eliminate options:
- Who maintains this in two years? If the answer is one non-technical founder and a contractor, a hand-rolled microservice mesh is malpractice.
- Where does it have to run? Shared hosting, a VPS you already pay for, or a cloud account with a budget alert set at $50 changes everything.
- What is the real traffic shape? Ten thousand users a month is not a scaling problem. Ten thousand concurrent websocket connections is.
- What data does it hold? Payment or health data pulls compliance requirements forward into the architecture.
- How fast does it need to ship? A six-week validation build and a five-year platform deserve different answers.
Nine times out of ten, writing those five answers down narrows the field to two or three sensible stacks. The remaining choice is then a matter of taste and team familiarity — which is a much smaller, much safer decision.
The frontend question: does it actually need to be an SPA?
We build plenty of React and Next.js applications, and we like them. But a lot of what gets built as a single-page app is a content site with a contact form. Server-rendered HTML with a sprinkle of JavaScript ships faster, ranks better, and breaks less.
Our rough rule: if the interface has genuine client-side state that persists across views — a dashboard, an editor, a live filtering UI — reach for a component framework. If each page is largely independent, server rendering with progressive enhancement is a better fit. Modern tooling makes the middle ground easy too. Islands, partial hydration, and HTMX-style patterns all let you keep a fast document and add interactivity only where it earns its cost.
The database is the decision you cannot undo
Frameworks are replaceable. Schemas are not. Data outlives every layer above it, so we spend most of our design time here.
Our default is PostgreSQL, and it stays the default until something specific forces a change. A common reason teams reach for a document store is flexible fields, but Postgres handles that natively with jsonb while keeping real constraints, joins, and transactions:
CREATE TABLE orders (
id bigserial PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers(id),
status text NOT NULL CHECK (status IN ('pending','paid','shipped')),
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX orders_metadata_gin ON orders USING gin (metadata);
-- Query the flexible part without giving up the rigid part
SELECT id, metadata->>'campaign' AS campaign
FROM orders
WHERE status = 'paid'
AND metadata @> '{"channel": "email"}';
That gives you schemaless convenience where you want it and referential integrity where you need it. It is one fewer service to run, back up, and monitor.
Design the seams, not the perfect choice
You will get something wrong. The goal is not a flawless first guess — it is making the wrong guess cheap to correct. We do that by keeping third-party services behind a thin boundary in our own code:
// payments/provider.ts
export interface PaymentProvider {
charge(amountCents: number, token: string): Promise<{ id: string }>;
refund(chargeId: string): Promise<void>;
}
// The rest of the app depends on this interface, never on the SDK.
export function createPayments(): PaymentProvider {
return process.env.PAYMENTS_DRIVER === 'stripe'
? new StripePayments(process.env.STRIPE_KEY!)
: new SandboxPayments();
}
It costs about twenty lines. It has saved us weeks on more than one migration.
Boring beats clever
The best stack is usually the one your team can debug at 2am. A well-understood framework with a large community, long-term support, and abundant hiring pool will outperform a faster, newer alternative that nobody on the team has run in production. We prefer technology with visible maintenance history, clear upgrade paths, and documentation written for people who did not attend the conference talk.
Two practical filters we apply to any candidate: can we deploy it with a single command, and can we roll it back just as easily? If either answer is no, the stack is not finished yet — regardless of how good the language is.
Where we land
For most business web projects, we end up somewhere near: a server-rendered or lightly hydrated frontend, a single application backend, PostgreSQL, object storage for files, and managed deployment with automated backups. It is not exciting. It ships in weeks, runs cheaply, and hands over cleanly to whoever comes next — which, in the end, is what the client actually bought.
If you are weighing options for a new build and want a second opinion grounded in what you will actually have to run and maintain, we are happy to talk it through. Get in touch with us here — no pitch, just a straight conversation about your constraints.