Search

Suggested keywords:
  • Java
  • Docker
  • Git
  • React
  • NextJs
  • Spring boot
  • Laravel

Streamlining Development with trpc.io: A Deep Dive into Efficient TypeScript APIs

  • Share this:

post-title

In the ever-evolving landscape of software development, the quest for efficiency is perpetual. Developers are constantly seeking tools and frameworks that not only simplify their workflow but also enhance productivity. One such tool that has garnered attention for its practicality and effectiveness is trpc.io—a TypeScript-first framework designed for buildin     g type-safe APIs. In this comprehensive exploration, we'll delve into the practical aspects of trpc.io, emphasizing its philosophy of prioritizing more code over excessive theoretical complexities.

Understanding trpc.io

TypeScript-First Approach

trpc.io distinguishes itself by adopting a TypeScript-first approach. This means leveraging the robust type system of TypeScript to ensure the clarity and correctness of API contracts. The emphasis on TypeScript is not just a feature; it's a fundamental principle that permeates every aspect of trpc.io development.

Minimalistic Design:

One of the hallmarks of trpc.io is its minimalistic design. Instead of drowning developers in a sea of configuration files and intricate annotations, trpc.io encourages a code-centric approach. API definitions are crafted directly in code, leading to cleaner, more readable, and inherently self-documenting codebases.

Practical Implementation:

Let's take a hands-on approach to understand how trpc.io simplifies API development in real-world scenarios.

Installation:

Getting started with trpc.io is a straightforward process. Install it using your preferred package manager:

npm install -D trpc

# or

yarn add -D trpc

 

Server Implementation

Defining APIs:

trpc.io allows developers to define APIs in a clear and concise manner. Consider the following example:

// server/index.ts

import { createTRPC } from 'trpc/server';

import * as z from 'zod';

const t = createTRPC();

export const getUser = t.procedure.input(z.object({ id: z.string() })).query(

  async ({ input }) => {

    // Fetch user logic here
    return { id: input.id, name: 'John Doe' };

  },

);

export const authenticateUser = t.procedure.input(

  z.object({ username: z.string(), password: z.string() }),

).mutation(async ({ input }) => {

  return { token: 'generated_token' };

});

export const getProductCatalog = t.procedure.query(async () => {

  return [{ id: '1', name: 'Product 1' }, { id: '2', name: 'Product 2' }];

});

export const placeOrder = t.procedure.input(

  z.object({ productId: z.string(), quantity: z.number() }),

).mutation(async ({ input }) => {

  return { orderId: 'generated_order_id' };

});

const appRouter = t.createExpressMiddleware();

const server = t.createHTTPServer(appRouter);

server.listen(3000, () => {

  console.log('Server is running on port 3000');

});

In the server code, appRouter is an instance of the trpc.io router, and createHTTPServer creates an HTTP server that uses appRouter. The server is then set to listen on port 3000. This represents a basic setup for handling HTTP requests using trpc.io.

 

Type Safety:

TypeScript's role in trpc.io shines when it comes to type safety. Autocompletion, type checking, and documentation are seamlessly integrated into your development environment. This not only enhances the developer experience but also significantly reduces the likelihood of runtime errors.

// Usage in your code

const user = await t.batch(

  getUser({ id: '123' }),

  // additional queries or mutations here

);

// TypeScript ensures 'user' is { id: string; name: string }

 

Advanced Usage: Building on trpc.io's Foundation

Middleware and Hooks:

trpc.io's extensibility is a key feature that allows developers to customize and extend its functionality. Middleware and hooks play a crucial role in enhancing and modifying the behavior of trpc.io queries and mutations.

// server/index.ts

//..( previous codes )

import { initTRPC } from 'trpc/server';

import * as z from 'zod';

const t = initTRPC();

// Middleware example

t.middleware((opts, next) => {

  // Custom logic before each query/mutation

  console.log('Executing middleware:', opts);

  // Continue to the next middleware or query/mutation

  return next(opts);

});


// Hook example

t.hook('beforeQuery', ({ input, ctx }) => {

  // Custom logic before executing each query

  console.log('Before Query:', input);

 // You can modify the context or input before execution

  return { input, ctx };

});


// Define a query with middleware and hooks

export const getUserWithMiddleware = t.procedure

  .input(z.object({ id: z.string() }))

  .query(

    async ({ input }) => {

      // Fetch user logic here

      return { id: input.id, name: 'John Doe' };

    },

    {

      middleware: async (opts, next) => {

        // Custom logic specific to this query

        console.log('Executing middleware for getUserWithMiddleware:', opts);

        // Continue to the next middleware or query

        return next(opts);

      },

      hooks: {

        beforeQuery: ({ input, ctx }) => {

          // Custom logic specific to this query before execution

          console.log('Before Query for getUserWithMiddleware:', input);

          // You can modify the context or input before execution

          return { input, ctx };

        },

      },

    },

  );

 

Batching Queries:

trpc.io provides a convenient way to batch multiple queries together, reducing the number of network requests. This can significantly improve the performance of your application.

Batching queries in the server for improved performance:

// server/index.ts

//..( previous codes )

// Usage of batching

const results = await t.batch(

  getUser({ id: '123' }),

  getProductCatalog(),

  placeOrder({ productId: '1', quantity: 2 }),

);

// Destructure results

const [user, productCatalog, order] = results;

 

By using the t.batch function, you can efficiently combine multiple queries into a single request. This is particularly useful in scenarios where you need to fetch data from different parts of your API in a single round trip.

Authentication and Authorization:

trpc.io makes it easy to integrate authentication and authorization into your API. You can define authentication and authorization middleware to secure your queries and mutations.

//server/index.ts

//..( previous codes )

// Define authentication middleware

t.middleware(({ input, ctx }, next) => {

  // Check if the user is authenticated

  if (!ctx.isAuthenticated) {

    throw new Error('Unauthorized: User not authenticated');

  }

 // Continue to the next middleware or query/mutation

  return next({ input, ctx });

});

// Usage in a secured query

export const getSecureData = t.procedure.query(

  async ({}, { ctx }) => {

    // Only authenticated users can access this data

    return { secureData: 'Sensitive information' };

  },

  {

    middleware: t.defaultAuthMiddleware(),

  },

);

 

Client Implementation

Installation

On the client side, install trpc.io:

npm install trpc

# or

yarn add trpc

 

Usage in Client Code

In the client directory (e.g., client/index.ts), call server functions:

// client/index.ts

import { createTRPCProxyClient } from 'trpc/client';

import { getUser, getUserWithMiddleware, getSecureData } from '../server';

/// Create a TRPC proxy client

const proxy = createTRPCProxyClient();

// Usage in your code

const user = await proxy.getUser({ id: '123' });

const secureData = await proxy.getSecureData();

// TypeScript ensures type safety

console.log(user); // { id: string; name: string }

console.log(secureData); // { secureData: string }

 

Case Study: trpc.io in Action

To illustrate the practical impact of trpc.io, let's consider a hypothetical scenario where a development team adopts trpc.io for an e-commerce application.

Scenario:

The team is tasked with building an API to manage user authentication, product catalog retrieval, and order processing. The goal is to create a robust and scalable API that can handle the demands of a growing e-commerce platform.

 

Server Implementation

// server/index.ts

// ... (previous server code)

export const authenticateUser = t.procedure.input(

  z.object({ username: z.string(), password: z.string() }),

).mutation(async ({ input }) => {

  return { token: 'generated_token' };

});

export const getProductCatalog = t.procedure.query(async () => {

  return [{ id: '1', name: 'Product 1' }, { id: '2', name: 'Product 2' }];

});

export const placeOrder = t.procedure.input(

  z.object({ productId: z.string(), quantity: z.number() }),

).mutation(async ({ input }) => {

  return { orderId: 'generated_order_id' };

});

const appRouter = t.createExpressMiddleware();

const server = t.createHTTPServer(appRouter);

server.listen(3000, () => {

  console.log('Server is running on port 3000');

});

 

Client Implementation

// client/index.ts

import { createTRPCProxyClient } from 'trpc/client';

import { authenticateUser, getProductCatalog, placeOrder } from '../server';

// Create a TRPC proxy client

const proxy = createTRPCProxyClient();

// Usage in your code

const token = await proxy.authenticateUser({ username: 'user', password: 'pass' });

const catalog = await proxy.getProductCatalog();

const order = await proxy.placeOrder({ productId: '1', quantity: 2 });

// TypeScript ensures type safety

console.log(token); // { token: string }

console.log(catalog); // { id: string; name: string }[]

console.log(order); // { orderId: string }

In this simplified example, the team leverages trpc.io to define authentication, product catalog retrieval, and order processing functionalities. The concise and expressive nature of trpc.io allows the team to focus on the specific logic of each operation without being burdened by extraneous details.

Benefits Observed:

Clarity in Codebase:

The codebase remains clear and focused on business logic, making it easier for developers to navigate and understand each API endpoint. Type Safety in Action: TypeScript ensures that the API contracts are adhered to, preventing potential errors related to data types and structures. Iterative Development: The team can quickly iterate on API endpoints, responding to changing requirements and adding new features with minimal friction. Reduced Bug Surface: Type safety and early error detection at the compilation stage significantly reduce the chances of runtime bugs, contributing to a more stable and reliable application.

 

Conclusion:

trpc.io, with its emphasis on a more code-centric development approach, stands out as a powerful and practical framework for building type-safe APIs. By prioritizing simplicity, clarity, and type safety, trpc.io empowers developers to create robust and efficient APIs with ease. The examples provided showcase not only the fundamentals of trpc.io but also its extensibility and seamless integration with frontend frameworks like React.

As developers continue to seek tools that align with the principles of efficiency and effectiveness, trpc.io remains a valuable asset in the modern developer's toolkit. Its pragmatic design, coupled with advanced features and integrations, positions trpc.io as a compelling choice for teams and individuals striving to streamline their API development process and build resilient, type-safe applications.

Nikhil Mishra

About author
Hi there! I'm learning full-stack web development and I document my journey through blogs and technical articles. I'm passionate about building web applications from the ground up and enjoy exploring all the different technologies involved in the process.