Skip to content
DevSaheb

service

Web applications that are still changeable in year three

Most web work sold as development is publishing: pages, a form, a content editor. That is a real need and a smaller job than this one. We build the other kind — applications that hold state, enforce rules, integrate with systems that were not designed to be integrated with, and are expected to still be maintainable when the people who wrote them have moved on.

The decision that costs the most later

Almost every expensive web rebuild traces back to the same early choice: where the truth lives. When business rules end up spread across the browser, the API and a handful of database triggers, every subsequent change requires understanding all three, and no single test can prove the rule holds.

We put rules in one place and make the other layers ask. That sounds obvious and is routinely abandoned under deadline pressure, because duplicating a validation in the browser is faster this week. It is also why the same feature takes four days in year one and three weeks in year three.

The practical version: a typed contract between client and server, generated rather than hand-written, so a field that changes shape on the server becomes a compile error in the browser rather than a runtime surprise for a user.

Performance is a budget, not an optimisation phase

Speed is not something added before launch. By then the architecture has already decided most of it — how much JavaScript ships, how many round trips the first screen needs, whether the layout settles or jumps.

So we set a budget at the start and enforce it in CI. If a pull request pushes the bundle past the limit, it fails, and the discussion happens while the change is small. Largest Contentful Paint under two seconds and Interaction to Next Paint under 200 ms are not aspirations on our projects; they are gates a release has to pass.

The same applies to accessibility. WCAG 2.2 AA is audited automatically on every build, which is the only way it survives contact with a schedule.

Built to be handed over

A web application you cannot staff is a liability regardless of how well it runs. We write for the team that inherits it: conventional structure over clever abstraction, dependencies chosen for how well maintained they are rather than how new, and documentation aimed at someone who has never spoken to us.

That also shapes the stack. We use widely known tools deliberately, because the pool of people who can pick the project up is part of its architecture.

TypeScript · shared/contracts/invoice.ts

The contract that stops the browser and the server drifting apart. One module defines the shape; the server validates its own response against it before sending, and the browser derives its type from the same definition rather than restating it. Rename a field on the server and the browser fails to compile in the same commit — instead of rendering undefined for a customer and being found weeks later by someone reading a bug report.

// shared/contracts/invoice.ts — imported by both halves. One definition.
import { z } from 'zod';

export const invoice = z.object({
  id: z.string().uuid(),
  reference: z.string(),
  totalMinor: z.number().int(),
  currency: z.enum(['BDT', 'USD', 'EUR']),
  dueOn: z.string().date(),
  settledAt: z.string().datetime().nullable(),
});

export type Invoice = z.infer<typeof invoice>;


// server/routes/invoices.ts
import { invoice, type Invoice } from '../../shared/contracts/invoice';

app.get('/api/invoices/:id', async (req, res) => {
  const row = await db.invoices.findById(req.params.id);
  if (!row) return res.status(404).json({ error: 'NOT_FOUND' });

  // Validated on the way OUT too. A column rename fails here, in our own
  // tests, rather than silently sending a field the browser does not expect.
  res.json(invoice.parse(toApiShape(row)));
});


// src/api/useInvoice.ts
import { invoice, type Invoice } from '../../shared/contracts/invoice';

export function useInvoice(id: string) {
  return useQuery<Invoice>({
    queryKey: ['invoice', id],
    queryFn: async ({ signal }) => {
      const res = await fetch(`/api/invoices/${id}`, { signal });
      if (!res.ok) throw new Error(`Invoice ${id}: ${res.status}`);
      // Not `as Invoice`. A cast trusts the server; this checks it.
      return invoice.parse(await res.json());
    },
  });
}

// Remove `reference` from the schema and every component reading
// invoice.reference stops compiling. That is the whole point of sharing it.

Who works on this

  • Nayemul Saheb

    CEO & Software Engineer

    Designs the client-server contract and builds both halves.

The rest of the team

When we would point you elsewhere

If you need a marketing site with a blog and a few landing pages, a well-chosen CMS or a site builder will serve you better and cost a fraction of this. Custom development earns its keep when the rules are yours, the integrations are awkward, or the thing has to scale in a way an off-the-shelf product cannot. If you are unsure which you have, the tell is usually whether you can describe the job in terms of pages or in terms of what the software must not allow to happen.

Common questions

Is this different from building a website?
Yes, and the difference is worth naming before anyone quotes. A website presents content; an application holds state and enforces rules. If what you need is pages, a content editor and a contact form, this is more engineering than the problem deserves and we will say so.
Do you work with our existing codebase?
Usually. We read it before proposing anything, and a rewrite is the recommendation of last resort — it restarts the clock on every bug that was already found and fixed. If we do recommend rebuilding, we will show you what we found rather than assert it.
Which framework will you use?
React with TypeScript for most applications, and Next.js where server rendering earns its complexity. The framework matters less than the boundaries around it; we keep business rules out of framework-specific code so that the framework is replaceable and the rules are not.
How do you handle browser support?
We agree the matrix at the architecture stage, because it changes what can be used and is expensive to retrofit. Then it is tested, not assumed — a support claim nobody verifies is a support claim that fails in the one browser your largest customer uses.

Related services

Tell us the constraint. We will tell you honestly if we fit.