Category: Web Development

  • TypeScript Tips & Tricks: Patterns That Separate Juniors from Seniors

    TypeScript has become the default choice for serious JavaScript development. With over 43% of developers using it (Stack Overflow 2025), its type system catches entire categories of bugs at compile time. But TypeScript’s power goes far beyond basic annotations — its advanced type system is a programming language in its own right. Here are techniques that separate proficient developers from beginners.

    Generics: Write Once, Type Everything

    // Generic API response wrapper — type-safe for any data shape
    interface ApiResponse<T> {
        status: number;
        data: T;
        timestamp: string;
    }
    
    function handleResponse<T>(response: ApiResponse<T>): T {
        if (response.status >= 400) throw new Error(`API error: ${response.status}`);
        return response.data;
    }
    
    interface User { id: string; name: string; email: string; }
    const userResp: ApiResponse<User> = { status: 200, data: { id: '1', name: 'Alice', email: 'alice@example.com' }, timestamp: new Date().toISOString() };
    const user = handleResponse(userResp);
    // user is typed as User — full autocomplete, full safety
    
    // Constrained generic — T must have an 'id' property
    function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
        return items.find(item => item.id === id);
    }

    Discriminated Unions

    By adding a literal type field, you get exhaustive pattern matching that eliminates impossible states:

    type FetchState<T> =
        | { type: 'idle' }
        | { type: 'loading'; startedAt: number }
        | { type: 'success'; data: T; fetchedAt: number }
        | { type: 'error'; message: string; retryCount: number };
    
    function renderState<T>(state: FetchState<T>): string {
        switch (state.type) {
            case 'idle':    return 'Ready';
            case 'loading': return `Loading... (${Date.now() - state.startedAt}ms)`;
            case 'success': return `Got: ${JSON.stringify(state.data)}`;
            case 'error':   return `Error: ${state.message} (retry ${state.retryCount}/3)`;
            // Remove a case → compile error. Impossible to forget a state.
        }
    }

    Utility Types You Should Know

    interface User { id: string; name: string; email: string; role: 'admin'|'editor'|'viewer'; createdAt: Date; }
    
    type UserUpdate = Partial<Omit<User, 'id' | 'createdAt'>>;  // All fields optional except id/createdAt
    type UserSummary = Pick<User, 'id' | 'name' | 'role'>;       // Just id, name, role
    type UserMap = Record<string, User>;                          // Typed lookup table
    type NotAdmin = Exclude<User['role'], 'admin'>;               // 'editor' | 'viewer'
    
    // ReturnType extracts a function's return type
    function createUser(name: string) { return { id: crypto.randomUUID(), name, createdAt: new Date() }; }
    type CreatedUser = ReturnType<typeof createUser>;

    Template Literal Types

    type Entity = 'user' | 'order' | 'product';
    type Action = 'created' | 'updated' | 'deleted';
    type EventName = `${Entity}:${Action}`;
    // 'user:created' | 'user:updated' | ... (9 combinations, all type-safe)

    The satisfies Operator

    Validates that a value matches a type WITHOUT widening its inferred type — best of both worlds:

    type Theme = Record<string, string | number>;
    const theme = {
        primary: '#6366f1',
        fontSize: 16,
    } satisfies Theme;
    theme.primary;   // Type: '#6366f1' (not string!)
    theme.fontSize;  // Type: 16 (not number!)

    Mapped & Conditional Types

    // Make all string properties nullable
    type Nullable<T> = { [K in keyof T]: T[K] extends string ? T[K] | null : T[K]; };
    
    // Deep readonly — recursively freeze nested objects
    type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]; };

    These patterns compound. Once you internalize generics, discriminated unions, utility types, and satisfies, you write code that’s simultaneously more flexible and more type-safe.

    Further reading: TypeScript Handbook | SO 2025 Survey

  • GraphQL API Tutorial: Build a Typed API from Scratch with Apollo Server

    GraphQL is a query language for APIs that lets clients request exactly the data they need. Unlike REST with fixed endpoints, GraphQL exposes a single endpoint with a typed schema. It solves over-fetching, under-fetching, and the N+1 endpoint problem. Here’s how to build one from scratch.

    Core Concepts

    A GraphQL API is defined by its schema — a strongly typed contract. Queries read data, Mutations write data, Subscriptions stream real-time updates. Each field has a resolver — a function returning the data. The execution engine calls resolvers in parallel and assembles the response.

    Building with Apollo Server

    const { ApolloServer } = require('@apollo/server');
    const { startStandaloneServer } = require('@apollo/server/standalone');
    const { GraphQLError } = require('graphql');
    
    const typeDefs = `#graphql
        type User { id: ID!; name: String!; email: String!; posts: [Post!]!; postCount: Int! }
        type Post { id: ID!; title: String!; body: String!; published: Boolean!; author: User! }
        input CreateUserInput { name: String!; email: String! }
        input CreatePostInput { title: String!; body: String!; authorId: ID!; published: Boolean = false }
        type Query { users: [User!]!; user(id: ID!): User; posts(published: Boolean): [Post!]! }
        type Mutation { createUser(input: CreateUserInput!): User!; createPost(input: CreatePostInput!): Post!; publishPost(id: ID!): Post! }
    `;
    
    let users = [
        { id: '1', name: 'Alice Chen', email: 'alice@example.com' },
        { id: '2', name: 'Bob Martinez', email: 'bob@example.com' },
    ];
    let posts = [
        { id: '1', title: 'GraphQL Basics', body: 'An intro...', published: true, authorId: '1' },
        { id: '2', title: 'Advanced Queries', body: 'Deep dive...', published: true, authorId: '1' },
    ];
    let nextId = { user: 3, post: 3 };
    
    const resolvers = {
        Query: {
            users: () => users,
            user: (_, { id }) => users.find(u => u.id === id) || (() => { throw new GraphQLError('Not found'); })(),
            posts: (_, { published }) => published !== undefined ? posts.filter(p => p.published === published) : posts,
        },
        Mutation: {
            createUser: (_, { input }) => {
                if (users.some(u => u.email === input.email)) throw new GraphQLError('Email exists');
                const user = { id: String(nextId.user++), ...input };
                users.push(user);
                return user;
            },
            createPost: (_, { input }) => {
                const post = { id: String(nextId.post++), ...input };
                posts.push(post);
                return post;
            },
            publishPost: (_, { id }) => { const p = posts.find(p => p.id === id); p.published = true; return p; },
        },
        User: {
            posts: (user) => posts.filter(p => p.authorId === user.id),
            postCount: (user) => posts.filter(p => p.authorId === user.id).length,
        },
        Post: { author: (post) => users.find(u => u.id === post.authorId) },
    };
    
    (async () => {
        const server = new ApolloServer({ typeDefs, resolvers });
        const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
        console.log(`GraphQL API at ${url}`);
    })();

    Querying the API

    # Single request replaces 2+ REST calls
    query { users { name postCount posts { title published } } }
    
    # Precise data fetching — only the fields you need
    query { user(id: "1") { name email posts { title body } } }
    
    # Mutations
    mutation { createUser(input: { name: "Charlie", email: "charlie@example.com" }) { id name } }
    mutation { createPost(input: { title: "New Post", body: "Content...", authorId: "1", published: true }) { id title author { name } } }

    N+1 Problem & DataLoader

    50 posts each resolving author = 51 queries. DataLoader batches and caches: collects all requested IDs, makes one batched query, distributes results.

    GraphQL vs REST

    Choose GraphQL for multiple client types with different data needs, complex nested relationships, or rapidly evolving frontends. Stick with REST for simple CRUD, file uploads, caching-heavy workloads, or teams without GraphQL experience. They can coexist — REST for simple resources, GraphQL for complex aggregation.

    Further reading: GraphQL Docs | Apollo Server Docs