Skip to main content
The Lacanians
Engineering7 min read

Why We Build With TypeScript

From frontend to backend, CLIs to infrastructure, here is how TypeScript and Bun support our application work -- and where we still choose something else.

A

Abdul Hamid Achik

·Updated

Why TypeScript Is Our Default

TypeScript is now a common default for application development, but adopting it is still an engineering decision rather than a badge of seriousness. Its value comes from the contracts it makes visible across a system and the tooling those contracts enable.

At The Lacanians, we use TypeScript for most frontend, backend, automation, and configuration work. We still reach for Go when a standalone binary or a different runtime profile better fits the problem. This post explains that default and its limits.

The Ecosystem Has Matured

TypeScript’s strength is the type system. The ecosystem around it – runtimes, frameworks, validation libraries, and database tooling – now lets those types travel through more of the application.

Bun Changes the Runtime Story

Node.js remains a capable runtime with the broadest compatibility surface. We use Bun when its consolidated development tooling fits the project:

# Initialize a project
bun init

# Install dependencies
bun install

# Run TypeScript directly during development
bun run src/index.ts

# Built-in test runner
bun test

# Bundle for production
bun build ./src/index.ts --outdir ./dist

Bun can execute TypeScript without a separate development transpile command and includes a test runner and bundler. It does not replace static type checking: we still run the TypeScript compiler or the framework’s type-checking command in development and CI.

We use Bun for internal tools such as our knowledge base, noted. Our secret management tool, tinyvault, runs on Go because its embedded storage and distribution model call for a different tradeoff. The point is not runtime loyalty; it is choosing the smallest dependable toolchain for the job.

Framework Convergence

The framework landscape has consolidated around a few excellent options, all TypeScript-first:

Astro for content and marketing sites. This site is built with Astro. The content collections API with Zod schema validation is exactly right for structured content:

// Type-safe content collections
const blog = defineCollection({
  schema: z.object({
    title: z.string(),
    pubDate: z.coerce.date(),
    category: z.enum(['Engineering', 'AI & Development', 'Startup']),
    tags: z.array(z.string()),
  }),
});

// Query with full type inference
const posts = await getCollection('blog');
// posts[0].data.category is typed as the enum, not string

Next.js and Nuxt for full-stack applications. Server components, API routes, middleware – all typed end-to-end. We use Nuxt for Vue projects and Next.js for React projects, choosing based on client preference and team familiarity.

NestJS for backend services that need structure. When a project outgrows a few API routes and needs proper dependency injection, request validation, and middleware pipelines, NestJS provides Rails-like structure with full TypeScript support.

Type Safety as a Contract Layer

The argument for types can sound abstract: “it catches bugs.” In practice, types can serve as a contract layer across an application.

Database to UI Type Safety

With tools like Drizzle ORM or Prisma, your database schema generates TypeScript types. Those types flow through your API layer, into your frontend components, and all the way to the rendered UI. A column rename in your database becomes a compile-time error in your React component.

// Schema definition (Drizzle)
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  plan: varchar('plan', { length: 50 }).$type<'free' | 'pro' | 'enterprise'>(),
});

// Inferred type flows everywhere
type User = typeof users.$inferSelect;
// { id: number; email: string; plan: 'free' | 'pro' | 'enterprise' | null }

// API endpoint -- return type is inferred
export async function getUser(id: number) {
  return db.select().from(users).where(eq(users.id, id));
}

// Component -- type errors if schema changes
function UserBadge({ user }: { user: User }) {
  // TypeScript knows user.plan is 'free' | 'pro' | 'enterprise' | null
  return <span className={planStyles[user.plan ?? 'free']}>{user.plan}</span>;
}

When a database field changes, TypeScript can surface affected references during development. That does not prove the application is correct, but it gives the team a concrete list to review before users encounter mismatched data shapes at runtime.

Types Give AI Tools Explicit Context

This is an underappreciated benefit. When you use AI tools like Claude or Copilot in a typed codebase, the quality of generated code improves dramatically. The AI reads your type definitions as specification. It knows what shape the data has, what functions accept, and what components expect.

In untyped JavaScript, an AI tool has fewer explicit constraints to inspect. In TypeScript, it can read interfaces and function signatures as part of the specification. That context can improve a generated first draft, but the type checker and human review still need to verify the result.

Our TypeScript Stack in Practice

Here is the concrete stack we reach for on most projects:

Layer Tool Why
Runtime Bun Speed, native TS, built-in tooling
Frontend Framework Astro / Next.js / Nuxt Depends on project type
Styling Tailwind CSS Utility-first, no context switching
Database PostgreSQL + Drizzle Type-safe queries, great migrations
API tRPC or NestJS End-to-end types or structured backend
Validation Zod Runtime + compile-time validation
Testing Vitest / Bun test Fast, TypeScript-native
CI/CD GitHub Actions Reliable, well-integrated

Every layer speaks TypeScript. When we define a Zod schema for form validation, that same schema validates API input, generates TypeScript types, and can even generate OpenAPI documentation. One definition, used everywhere.

The Go Exception

We are not purists. Some problems are better solved in Go, and we reach for it when appropriate. Our CLI framework nexo is built in Go because CLIs benefit from single-binary distribution, minimal startup time, and low memory overhead. Go delivers all three.

The decision framework is simple:

  • TypeScript when you need rapid iteration, a rich ecosystem of libraries, or full-stack web development
  • Go when you need compiled binaries, maximum concurrency, or minimal resource usage

For most application work we take on, TypeScript is the default. It is not a requirement when another language better fits the distribution, concurrency, resource, or ecosystem constraints.

Practical Advice for Teams

If you are starting a new project, here is our opinionated advice:

Evaluate Bun against your dependencies and deployment target. It can simplify the development toolchain, but compatibility requirements should decide whether it is the right runtime. Keep type checking explicit in CI either way.

Pick one framework and commit. Do not use Next.js for some pages and Astro for others. Pick the one that matches your primary use case and build everything in it.

Invest in your type definitions. Spend deliberate time on your Zod schemas, database types, and API contracts. These are not just boilerplate; they describe the boundaries that the rest of the application depends on.

Use strict mode. Set "strict": true in your tsconfig.json and never turn it off. The short-term pain of satisfying the type checker prevents the long-term pain of runtime errors.

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Use types to constrain AI-assisted implementation. Define the interfaces, schemas, and function signatures before asking an AI tool for a first draft. Humans still own the contracts and review the behavior; TypeScript verifies structural compatibility, not product intent.

TypeScript Is Infrastructure

TypeScript is more than a language choice in our stack. It is the contract layer that connects the database to the UI and gives tools – including AI assistants – more explicit context about the system.

We use it by default because those contracts make AI-assisted development, refactoring, and production review easier to reason about. It does not replace tests or judgment, but it gives both a clearer surface to work against.

If you need a product stack with explicit contracts, validation, and an ownership handoff, see our product engineering service.

A

Abdul Hamid Achik

Founder and lead engineer at The Lacanians. Abdul builds production software, developer tools, and local-first systems from Guadalajara for teams worldwide.