Phase 1: Web Fundamentals & JavaScript

Shared types between frontend and backend

Intermediate ~3 min read
Think of it this way A friendly analogy. Read this if the technical version feels dense. Show Hide

Imagine you're playing a super fun board game, and your friend is the Game Master. You're like the "frontend" because you see the board, roll the dice, and move your pieces. Your friend is the "backend" because they know all the secret rules, keep track of scores, and make sure everything works correctly behind the scenes. Now, what if the rulebook for your game pieces was a bit different for you than for your friend? Maybe your rulebook says a "Gold Coin" has a value of 5, but your friend's Game Master rulebook says it has a worth of 5. Or you expect your "Wizard" character to have a magic_power number, but your friend's rules only mention spell_strength. Even if they mean the same thing, using different words can cause big problems!

This is exactly what happens in programming when we talk about "shared types." In coding, the "types" are like those descriptions for your game pieces – they say exactly what a "Gold Coin" or a "Wizard" looks like, what information it holds, and what it's called. When we talk about "sharing types" between the "frontend" (what you see) and "backend" (the game master), it just means making sure both sides use the exact same rulebook for all the pieces and cards in the game. Instead of having two slightly different sets of rules that describe a "Gold Coin," there’s just one official description that everyone agrees on.

Think about it: if the official game piece rulebook says "Gold Coin" has a value of 5 and a shiny property (yes/no), then both you (the frontend) and your friend (the backend Game Master) will look at that exact same page in the rulebook. You both know it's value not worth, and you both expect to see if it's shiny or not. Practically, this means putting all those important rulebook pages (the 'types') in one special folder that both your game setup (frontend) and your Game Master's hidden rules (backend) can look at. So, when the backend wants to tell you about a "Gold Coin," it checks the official rulebook, and when you receive it on your board, you also check the same official rulebook to understand it. No mix-ups!

So, when programmers build a super cool full-stack application (like a giant, complex board game!), using shared types means both the visible parts and the secret rule-checking parts always speak the exact same language about the game pieces and information. This makes sure the game runs smoothly, without anyone getting confused or making mistakes because they were reading different rulebooks. This way, you can be sure that when the backend sends you a "Wizard" piece, it has magic_power and not some other name you don't recognize, making your code much easier to build and much less likely to have unexpected glitches!

When building full-stack applications with TypeScript, your frontend and backend often deal with the exact same data structures—think a Product item, an Order object, or a User profile. If you define these types separately within your frontend and backend projects, you introduce a significant risk of inconsistencies. For instance, the frontend might expect product.imageURL while the backend inadvertently sends product.imageUrl, leading to runtime errors that TypeScript was designed to prevent. Sharing types means establishing a single, authoritative source of truth for these data shapes, ensuring both parts of your application speak the exact same language regarding data contracts.

Practically, sharing types typically involves placing your core interfaces and types in a common location accessible by both your frontend and backend codebases. In a monorepo setup, this is often a dedicated packages/shared or src/common/types directory that both projects can import from. For projects with separate repositories, you might create a dedicated TypeScript package, publish it to a private NPM registry, and then install it as a dependency in both your frontend and backend projects. The crucial point is that interfaces like Product or User are defined once, preventing redundant definitions and potential divergence as your application evolves.

The immediate benefit of this approach is robust type-safety across your entire stack. Your backend knows precisely what data to send according to the shared contract, and your frontend knows exactly what data shape to expect from API responses, drastically reducing bugs related to data shape mismatches. Developers also gain invaluable benefits like intelligent autocompletion in their IDEs for API responses and confidence during refactoring, knowing that type changes in the shared definition will immediately flag issues in consuming codebases on both sides. It streamlines development, makes API contracts explicit, and greatly improves the maintainability of complex applications.

Key Takeaways

  • Establish a single source of truth for shared data structures.
  • Prevents data shape mismatches and runtime errors between frontend and backend.
  • Common strategies: monorepo shared directory or a dedicated types-only NPM package.
  • Enhances developer experience with autocompletion, type safety, and refactoring confidence.

Code Example

typescript
Preview

How this code works

This code illustrates how to establish "shared types" that can be consistently used across both the frontend and backend of an application. By defining the Product interface in a common location, the entire system agrees on the exact structure of product data—its properties and their types (e.g., id as string, price as number). This unified definition ensures data consistency, preventing mismatches and bugs that can arise when different parts of an application expect data in varying formats.

The backend's createProduct function uses Omit<Product, 'id'> for its input, which is a subtle but important detail. It means that when a new product is initially sent to the backend for creation, the id property is omitted because the backend is responsible for generating it. Once the product is saved, the backend then returns a complete Product object, now including the newly generated id. The ProductCard component on the frontend then imports the same Product interface, expecting this complete object, allowing it to reliably access and display properties like product.name and product.price.