← Back to DevBytes

Hapi TypeScript: Strongly Typed Applications

Introduction to Hapi with TypeScript

Hapi (short for Http API) is a powerful Node.js framework designed for building scalable web applications, APIs, and services. When combined with TypeScript, Hapi becomes an even more formidable toolβ€”giving you end-to-end type safety, superior developer ergonomics, and self-documenting codebases that catch errors at compile time rather than at runtime.

This tutorial walks you through building a fully typed Hapi application from scratch. You will learn how to set up a project, define typed routes, leverage request and response validation, work with plugins, and adopt best practices that make your applications robust and maintainable.

Why Strong Typing Matters in Hapi

πŸš€ Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

JavaScript's dynamic nature allows rapid prototyping but can lead to subtle bugs: misspelled route parameters, incorrect payload shapes, or mismatched plugin configurations often surface only when requests hit your server. TypeScript changes this equation entirely.

The official @hapi/hapi package includes TypeScript type definitions out of the box, and the companion packages like @hapi/joi and @hapi/boom are also fully typed. This makes adopting TypeScript with Hapi seamless.

Project Setup

Begin by creating a new directory and initializing a Node.js project with TypeScript support.

mkdir hapi-ts-tutorial
cd hapi-ts-tutorial
npm init -y

Install the core dependencies:

npm install @hapi/hapi @hapi/joi @hapi/boom
npm install --save-dev typescript @types/node ts-node nodemon

Create a tsconfig.json file at the project root. This configuration targets modern Node.js versions and enables strict type checking.

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Add the following scripts to your package.json for development convenience:

"scripts": {
  "build": "tsc",
  "dev": "nodemon --exec ts-node src/server.ts",
  "start": "node dist/server.js"
}

Creating a Strongly Typed Hapi Server

Create the src directory and add the main server file. Here we define the server configuration with explicit types and register a basic route.

// src/server.ts
import Hapi from '@hapi/hapi';

interface ServerConfig {
  host: string;
  port: number;
}

const serverConfig: ServerConfig = {
  host: process.env.HOST || '0.0.0.0',
  port: Number(process.env.PORT) || 3000,
};

async function initServer(): Promise {
  const server: Hapi.Server = Hapi.server({
    host: serverConfig.host,
    port: serverConfig.port,
    router: {
      isCaseSensitive: false,
      stripTrailingSlash: true,
    },
  });

  // Register a simple health-check route
  server.route({
    method: 'GET',
    path: '/health',
    handler: (_request: Hapi.Request, h: Hapi.ResponseToolkit) => {
      return h.response({
        status: 'ok',
        timestamp: new Date().toISOString(),
      }).code(200);
    },
  });

  return server;
}

async function start(): Promise {
  try {
    const server = await initServer();
    await server.start();
    console.log(`Server running on ${server.info.uri}`);
  } catch (err) {
    console.error('Failed to start server:', err);
    process.exit(1);
  }
}

start();

Notice how Hapi.Server, Hapi.Request, and Hapi.ResponseToolkit are explicitly typed. TypeScript now ensures that every method call and property access on these objects is valid.

Typed Route Parameters and Payloads

Raw requests carry unknown data. Hapi's built-in validation powered by Joi lets you define schemas that parse and validate incoming parameters, payloads, and headers. With TypeScript, you can go further by defining interfaces for these validated shapes and using them throughout your handlers.

Defining Validation Schemas with Joi

// src/schemas/userSchemas.ts
import Joi from '@hapi/joi';

export const createUserPayloadSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).max(128).required(),
  role: Joi.string().valid('admin', 'editor', 'viewer').default('viewer'),
});

export const userIdParamsSchema = Joi.object({
  userId: Joi.string().uuid().required(),
});

export const userQuerySchema = Joi.object({
  search: Joi.string().optional(),
  limit: Joi.number().integer().min(1).max(100).default(20),
  offset: Joi.number().integer().min(0).default(0),
});

Mapping Joi Schemas to TypeScript Interfaces

While Joi validates at runtime, TypeScript interfaces enforce correct usage at compile time. You derive interfaces from your schemas to keep both in sync.

// src/types/userTypes.ts

export interface CreateUserPayload {
  username: string;
  email: string;
  password: string;
  role?: 'admin' | 'editor' | 'viewer';
}

export interface UserResponse {
  id: string;
  username: string;
  email: string;
  role: string;
  createdAt: string;
}

export interface UserQueryParams {
  search?: string;
  limit?: number;
  offset?: number;
}

export interface UserRouteParams {
  userId: string;
}

Building Typed Route Handlers

Now wire everything together in a route configuration. The handler receives validated data, and TypeScript guarantees that you access only properties that actually exist on the validated payload.

// src/routes/userRoutes.ts
import Hapi from '@hapi/hapi';
import { createUserPayloadSchema, userIdParamsSchema, userQuerySchema } from '../schemas/userSchemas';
import type { CreateUserPayload, UserResponse, UserQueryParams, UserRouteParams } from '../types/userTypes';

// In-memory store for demonstration
const users: Map = new Map();

export const userRoutes: Hapi.ServerRoute[] = [
  {
    method: 'POST',
    path: '/users',
    options: {
      validate: {
        payload: createUserPayloadSchema,
      },
      response: {
        status: {
          201: {
            description: 'User created successfully',
          },
        },
      },
    },
    handler: (request: Hapi.Request, h: ResponseToolkit): Hapi.ResponseObject => {
      // TypeScript now knows the shape of request.payload
      const payload = request.payload as CreateUserPayload;

      const newUser: UserResponse = {
        id: crypto.randomUUID(),
        username: payload.username,
        email: payload.email,
        role: payload.role ?? 'viewer',
        createdAt: new Date().toISOString(),
      };

      users.set(newUser.id, newUser);

      return h.response(newUser).code(201);
    },
  },

  {
    method: 'GET',
    path: '/users',
    options: {
      validate: {
        query: userQuerySchema,
      },
    },
    handler: (request: Hapi.Request, h: Hapi.ResponseToolkit): Hapi.ResponseObject => {
      const query = request.query as UserQueryParams;
      const limit = query.limit ?? 20;
      const offset = query.offset ?? 0;
      const search = query.search?.toLowerCase();

      let results = Array.from(users.values());

      if (search) {
        results = results.filter(
          (user) =>
            user.username.toLowerCase().includes(search) ||
            user.email.toLowerCase().includes(search)
        );
      }

      const paginated = results.slice(offset, offset + limit);

      return h.response({
        items: paginated,
        total: results.length,
        limit,
        offset,
      });
    },
  },

  {
    method: 'GET',
    path: '/users/{userId}',
    options: {
      validate: {
        params: userIdParamsSchema,
      },
    },
    handler: (request: Hapi.Request, h: Hapi.ResponseToolkit): Hapi.ResponseObject => {
      const params = request.params as UserRouteParams;
      const user = users.get(params.userId);

      if (!user) {
        return h.response({ error: 'User not found' }).code(404);
      }

      return h.response(user).code(200);
    },
  },

  {
    method: 'DELETE',
    path: '/users/{userId}',
    options: {
      validate: {
        params: userIdParamsSchema,
      },
    },
    handler: (request: Hapi.Request, h: Hapi.ResponseToolkit): Hapi.ResponseObject => {
      const params = request.params as UserRouteParams;
      const existed = users.delete(params.userId);

      if (!existed) {
        return h.response({ error: 'User not found' }).code(404);
      }

      return h.response().code(204);
    },
  },
];

By casting request.payload, request.query, and request.params to their respective interfaces, TypeScript provides full autocompletion and type checking inside every handler. If you attempt to access a property that doesn't exist on the interface, the compiler immediately reports an error.

Registering Routes on the Server

Update server.ts to import and register the typed routes.

// Updated section in src/server.ts
import { userRoutes } from './routes/userRoutes';

async function initServer(): Promise {
  const server: Hapi.Server = Hapi.server({
    host: serverConfig.host,
    port: serverConfig.port,
    router: {
      isCaseSensitive: false,
      stripTrailingSlash: true,
    },
  });

  // Register the health-check route
  server.route({
    method: 'GET',
    path: '/health',
    handler: (_request: Hapi.Request, h: Hapi.ResponseToolkit) => {
      return h.response({
        status: 'ok',
        timestamp: new Date().toISOString(),
      }).code(200);
    },
  });

  // Register all user routes
  server.route(userRoutes);

  return server;
}

Strongly Typed Plugins

Hapi's plugin system is one of its most powerful features. Plugins can add routes, decorate the server object, or extend request lifecycles. With TypeScript, you can define precise plugin interfaces so that consumers know exactly what a plugin provides.

Creating a Typed Plugin

// src/plugins/auditPlugin.ts
import Hapi from '@hapi/hapi';

interface AuditPluginOptions {
  logDestination: 'console' | 'file';
  logLevel: 'info' | 'debug' | 'warn' | 'error';
  excludePaths?: string[];
}

// Augment the Hapi module to include our plugin's decorations
declare module '@hapi/hapi' {
  interface ServerApplicationState {
    auditLogCount: number;
  }
  interface RequestApplicationState {
    requestStartTime: number;
  }
}

export const auditPlugin: Hapi.Plugin = {
  name: 'audit-plugin',
  version: '1.0.0',
  once: true,

  register: async function (
    server: Hapi.Server,
    options: AuditPluginOptions
  ): Promise {
    const { logDestination, logLevel, excludePaths = [] } = options;

    // Initialize server-level state
    server.app.auditLogCount = 0;

    // Register a server extension point
    server.ext('onRequest', (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
      // Attach per-request metadata
      request.app.requestStartTime = Date.now();
      return h.continue;
    });

    server.ext('onPreResponse', (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
      const path = request.path;
      const method = request.method.toUpperCase();

      // Skip excluded paths
      if (excludePaths.some((excluded) => path.startsWith(excluded))) {
        return h.continue;
      }

      const duration = Date.now() - request.app.requestStartTime;
      server.app.auditLogCount += 1;

      const logEntry = {
        method,
        path,
        statusCode: (request.response as Hapi.ResponseObject)?.statusCode ?? 200,
        durationMs: duration,
        timestamp: new Date().toISOString(),
      };

      if (logDestination === 'console') {
        const logFn = logLevel === 'error' ? console.error : console.log;
        logFn(`[AUDIT] ${JSON.stringify(logEntry)}`);
      }
      // In production, you'd write to a file here

      return h.continue;
    });
  },
};

Registering the Plugin with Type Safety

When you register the plugin, TypeScript enforces that you provide options matching the AuditPluginOptions interface.

// In server.ts, inside initServer():
await server.register({
  plugin: auditPlugin,
  options: {
    logDestination: 'console',
    logLevel: 'info',
    excludePaths: ['/health'],
  } as AuditPluginOptions,
});

If you omit a required option or provide an invalid value, TypeScript catches the mistake during compilationβ€”before the server even starts.

Typed Lifecycle Hooks and Extension Points

Hapi provides a rich request lifecycle with extension points like onRequest, onPreAuth, onPostHandler, onPreResponse, and more. You can type these hooks to work seamlessly with your application's data structures.

// src/extensions/authExtension.ts
import Hapi from '@hapi/hapi';
import Boom from '@hapi/boom';

interface AuthenticatedUser {
  userId: string;
  username: string;
  role: string;
}

// Augment Request interface to include authenticated user
declare module '@hapi/hapi' {
  interface Request {
    authenticatedUser?: AuthenticatedUser;
  }
}

export function setupAuthExtension(server: Hapi.Server): void {
  server.ext('onPreAuth', (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
    // Simulate extracting user from an Authorization header
    const authHeader = request.headers.authorization;

    if (authHeader && authHeader.startsWith('Bearer ')) {
      const token = authHeader.slice(7);

      // In a real app, you'd verify the JWT here
      // For demonstration, we decode a mock token
      try {
        const decoded = JSON.parse(Buffer.from(token, 'base64').toString('utf-8')) as AuthenticatedUser;
        request.authenticatedUser = decoded;
      } catch {
        // Token is invalid; let the route's auth strategy handle it
      }
    }

    return h.continue;
  });
}

Now any route handler can access request.authenticatedUser with full type safety:

// In a route handler:
handler: (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
  const user = request.authenticatedUser;

  if (!user) {
    return Boom.unauthorized('You must be logged in to access this resource');
  }

  // TypeScript knows user has userId, username, and role
  console.log(`Request from ${user.username} (${user.role})`);

  return h.response({ message: `Welcome, ${user.username}!` });
}

Typed Response Validation

Hapi can validate outgoing responses against Joi schemas. This ensures that your API never accidentally leaks extra fields or returns malformed data. Combined with TypeScript interfaces, you get a powerful dual-layer safety net.

// src/schemas/responseSchemas.ts
import Joi from '@hapi/joi';

export const userResponseSchema = Joi.object({
  id: Joi.string().uuid().required(),
  username: Joi.string().required(),
  email: Joi.string().email().required(),
  role: Joi.string().required(),
  createdAt: Joi.date().iso().required(),
}).unknown(false); // .unknown(false) disallows extra fields

export const userListResponseSchema = Joi.object({
  items: Joi.array().items(userResponseSchema).required(),
  total: Joi.number().integer().required(),
  limit: Joi.number().integer().required(),
  offset: Joi.number().integer().required(),
});

Apply these schemas in your route options:

{
  method: 'GET',
  path: '/users/{userId}',
  options: {
    validate: {
      params: userIdParamsSchema,
    },
    response: {
      status: {
        200: {
          description: 'User found',
          schema: userResponseSchema,
        },
      },
    },
  },
  handler: (request: Hapi.Request, h: Hapi.ResponseToolkit): Hapi.ResponseObject => {
    // ... handler logic
  },
}

If the handler returns an object that doesn't match userResponseSchema, Hapi throws a validation error. In development mode, this catches bugs early; in production, you can configure Hapi to strip extra fields or log the mismatch without exposing details to clients.

Structuring a Large TypeScript Hapi Project

As your application grows, a clear project structure keeps code maintainable. Here is a recommended layout for a strongly typed Hapi application:

src/
β”œβ”€β”€ server.ts                  # Server entry point and composition
β”œβ”€β”€ config/
β”‚   └── serverConfig.ts        # Typed server configuration
β”œβ”€β”€ plugins/
β”‚   β”œβ”€β”€ auditPlugin.ts         # Custom plugins with typed options
β”‚   └── authPlugin.ts          # Authentication plugin
β”œβ”€β”€ routes/
β”‚   β”œβ”€β”€ userRoutes.ts          # User CRUD routes
β”‚   └── healthRoutes.ts        # Health check and metrics routes
β”œβ”€β”€ schemas/
β”‚   β”œβ”€β”€ userSchemas.ts         # Joi validation schemas
β”‚   └── responseSchemas.ts     # Response validation schemas
β”œβ”€β”€ types/
β”‚   β”œβ”€β”€ userTypes.ts           # TypeScript interfaces
β”‚   └── commonTypes.ts         # Shared utility types
β”œβ”€β”€ services/
β”‚   └── userService.ts         # Business logic layer
β”œβ”€β”€ extensions/
β”‚   └── authExtension.ts       # Request lifecycle extensions
└── utils/
    └── helpers.ts             # Shared utility functions

Each layer has a clear responsibility. Types live in types/, runtime validation in schemas/, route definitions in routes/, and business logic in services/. This separation prevents circular dependencies and makes unit testing straightforward.

Best Practices for Hapi TypeScript Applications

1. Keep Runtime Validation and Types in Sync

Your Joi schemas and TypeScript interfaces represent the same contractβ€”one at runtime, one at compile time. When you update a payload shape, update both. Consider using tools like joi-to-typescript to auto-generate interfaces from Joi schemas in larger projects.

2. Use Module Augmentation Sparingly

Augmenting @hapi/hapi module declarations (as shown with Request.authenticatedUser) is powerful but can lead to confusion if multiple plugins augment the same interfaces. Document augmentations clearly and keep them centralized in a single types/augmentations.ts file.

3. Prefer Hapi.ServerRoute for Route Arrays

When exporting route arrays, type them as Hapi.ServerRoute[]. This ensures that every route object conforms to Hapi's expected shape and catches missing properties like method or path early.

4. Enable Strict TypeScript Options

Always enable "strict": true in tsconfig.json. This activates noImplicitAny, strictNullChecks, and other flags that catch common bugs. Combined with Hapi's runtime validation, strict mode creates a formidable safety net.

5. Type Error Responses with Boom

The @hapi/boom library provides typed HTTP error objects. Use it consistently for error responses rather than throwing plain objects:

import Boom from '@hapi/boom';

// Inside a route handler:
if (!resource) {
  throw Boom.notFound('The requested resource does not exist');
}

if (!authorized) {
  throw Boom.forbidden('You do not have permission to access this resource');
}

Boom errors integrate with Hapi's error lifecycle and produce consistent, well-typed error responses.

6. Leverage Server Decorations for Shared State

Instead of global variables, use server.app and request.app for typed shared state. Augment the corresponding interfaces so TypeScript knows what properties are available.

7. Write Tests with Type Safety

Use testing frameworks like Jest with ts-jest to write typed tests. Hapi's server.inject method is fully typed and lets you simulate requests without starting the actual server:

// src/__tests__/userRoutes.test.ts
import Hapi from '@hapi/hapi';
import { userRoutes } from '../routes/userRoutes';

describe('User Routes', () => {
  let server: Hapi.Server;

  beforeEach(async () => {
    server = Hapi.server();
    server.route(userRoutes);
    await server.initialize();
  });

  it('should create a user and return 201', async () => {
    const response = await server.inject({
      method: 'POST',
      url: '/users',
      payload: {
        username: 'johndoe',
        email: 'john@example.com',
        password: 'securePassword123',
        role: 'editor',
      },
    });

    expect(response.statusCode).toBe(201);
    const body = JSON.parse(response.payload);
    expect(body.username).toBe('johndoe');
    expect(body.email).toBe('john@example.com');
    expect(body.role).toBe('editor');
    expect(body.id).toBeDefined();
  });

  it('should return 404 for a non-existent user', async () => {
    const response = await server.inject({
      method: 'GET',
      url: '/users/00000000-0000-0000-0000-000000000000',
    });

    expect(response.statusCode).toBe(404);
  });
});

TypeScript validates your test assertions and catches mismatched property names in payloads and expected responses.

Advanced Patterns: Generic Route Builders

For large APIs with repetitive CRUD patterns, you can create generic route builders that encapsulate common logic while preserving type safety.

// src/builders/crudBuilder.ts
import Hapi from '@hapi/hapi';
import Joi from '@hapi/joi';

interface CrudConfig {
  resourceName: string;
  basePath: string;
  createSchema: Joi.ObjectSchema;
  updateSchema: Joi.ObjectSchema;
  idSchema: Joi.ObjectSchema;
  listQuerySchema: Joi.ObjectSchema;
  responseSchema: Joi.ObjectSchema;
  service: {
    create: (payload: CreatePayload) => Promise;
    getById: (id: string) => Promise;
    list: (query: any) => Promise<{ items: T[]; total: number }>;
    update: (id: string, payload: UpdatePayload) => Promise;
    delete: (id: string) => Promise;
  };
}

export function buildCrudRoutes(
  config: CrudConfig
): Hapi.ServerRoute[] {
  return [
    {
      method: 'POST',
      path: config.basePath,
      options: {
        validate: { payload: config.createSchema },
        response: { status: { 201: { schema: config.responseSchema } } },
      },
      handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
        const created = await config.service.create(request.payload as C);
        return h.response(created).code(201);
      },
    },
    {
      method: 'GET',
      path: `${config.basePath}/{id}`,
      options: {
        validate: { params: config.idSchema },
        response: { status: { 200: { schema: config.responseSchema } } },
      },
      handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
        const { id } = request.params as { id: string };
        const item = await config.service.getById(id);
        if (!item) throw Boom.notFound(`${config.resourceName} not found`);
        return h.response(item).code(200);
      },
    },
    {
      method: 'GET',
      path: config.basePath,
      options: {
        validate: { query: config.listQuerySchema },
      },
      handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
        const result = await config.service.list(request.query);
        return h.response(result).code(200);
      },
    },
    {
      method: 'PUT',
      path: `${config.basePath}/{id}`,
      options: {
        validate: {
          params: config.idSchema,
          payload: config.updateSchema,
        },
        response: { status: { 200: { schema: config.responseSchema } } },
      },
      handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
        const { id } = request.params as { id: string };
        const updated = await config.service.update(id, request.payload as U);
        if (!updated) throw Boom.notFound(`${config.resourceName} not found`);
        return h.response(updated).code(200);
      },
    },
    {
      method: 'DELETE',
      path: `${config.basePath}/{id}`,
      options: {
        validate: { params: config.idSchema },
      },
      handler: async (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
        const { id } = request.params as { id: string };
        const deleted = await config.service.delete(id);
        if (!deleted) throw Boom.notFound(`${config.resourceName} not found`);
        return h.response().code(204);
      },
    },
  ];
}

This builder preserves full type safety: the generic parameters T, C, and U flow from the schemas through to the service methods, ensuring that the payload types match the service expectations. You can generate entire resource endpoints with a single configuration object.

Handling Errors with Typed Lifecycle Methods

Hapi's request lifecycle includes onPreResponse, which is the ideal place to format errors consistently. You can create a typed error handler that transforms Boom errors and validation errors into a uniform API response.

// src/extensions/errorFormatter.ts
import Hapi from '@hapi/hapi';
import Boom from '@hapi/boom';

interface FormattedErrorResponse {
  statusCode: number;
  error: string;
  message: string;
  details?: Record;
  timestamp: string;
}

export function setupErrorFormatter(server: Hapi.Server): void {
  server.ext('onPreResponse', (request: Hapi.Request, h: Hapi.ResponseToolkit) => {
    const response = request.response;

    // Only format error responses
    if (!(response instanceof Error) && !Boom.isBoom(response)) {
      return h.continue;
    }

    // Handle Boom errors
    if (Boom.isBoom(response)) {
      const boomResponse = response as Boom.Boom;
      const formatted: FormattedErrorResponse = {
        statusCode: boomResponse.output.statusCode,
        error: boomResponse.output.payload.error,
        message: boomResponse.output.payload.message,
        timestamp: new Date().toISOString(),
      };

      return h.response(formatted).code(boomResponse.output.statusCode);
    }

    // Handle validation errors
    if ((response as any).isJoi) {
      const formatted: FormattedErrorResponse = {
        statusCode: 400,
        error: 'Validation Error',
        message: (response as any).message || 'Invalid request parameters',
        details: (response as any).details || {},
        timestamp: new Date().toISOString(),
      };

      return h.response(formatted).code(400);
    }

    // Fallback for unhandled errors
    const formatted: FormattedErrorResponse = {
      statusCode: 500,
      error: 'Internal Server Error',
      message: 'An unexpected error occurred',
      timestamp: new Date().toISOString(),
    };

    return h.response(formatted).code(500);
  });
}

Register this extension early in your server initialization so it wraps all routes:

// In initServer():
setupErrorFormatter(server);

Conclusion

Combining Hapi with TypeScript elevates your API development from trusting runtime behavior to guaranteeing compile-time correctness. Strong typing permeates every layer of a Hapi application: server configuration, route parameters, payload validation, plugin options, lifecycle hooks, and even error formatting. By defining clear interfaces, augmenting Hapi's built-in types judiciously, and keeping runtime Joi schemas aligned with your TypeScript types, you build applications that are safer to refactor, easier to onboard new developers onto, and dramatically less prone to production surprises. The patterns covered in this tutorialβ€”typed routes, generic CRUD builders, plugin type augmentation, and structured error handlingβ€”provide a solid foundation for any Hapi TypeScript project, whether a small microservice or a sprawling enterprise API.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles