Integrating ServiceTitan with Next.js: A Step-by-Step Guide to Building Custom Integrations

by Spencer, Co-Founder / CTO

Introduction

Integrating ServiceTitan with Next.js: A Comprehensive Guide to Modernizing Your Service Business

In the service industry, companies like HVAC, electrical, and plumbing businesses often rely on software solutions to manage their operations. However, off-the-shelf systems may not always meet specific business needs, leading to inefficiencies and limitations. Custom integrations can bridge this gap, enabling you to tailor your tools to fit your unique workflows.

Building custom integrations empowers you to optimize your business processes on your own terms.

This tutorial demonstrates how to create a custom booking form that integrates ServiceTitan with a Next.js application. By following this guide, you'll learn how to build key business flows into a custom web app, leveraging the power of ServiceTitan's API and the flexibility of Next.js.

What You'll Learn:

  • Setting Up a Next.js Application: Lay the groundwork for your custom integration.
  • Building a Custom Booking Form: Collect essential customer information with a user-friendly interface.
  • Connecting to ServiceTitan's API: Authenticate and interact with ServiceTitan programmatically.
  • Implementing Best Practices: Ensure your integration is secure, efficient, and scalable.

By the end of this guide, you'll have the tools and knowledge to build custom integrations that enhance your operations and cater to your specific business needs.

At Aclarify, we specialize in developing custom software solutions for service businesses. Our expertise in integrating ServiceTitan with modern web applications positions us as a trusted partner in taking on the complexities of custom development.

Ready to build your custom integration? Let's get started!


Prerequisites

Before diving into the tutorial, let's make sure you have everything in place to follow along smoothly.

Technical Knowledge

This guide assumes you're comfortable with the following:

  • JavaScript and React: Understanding of components, state management, and JSX.
  • Next.js Framework: Familiarity with Next.js features like pages, components, and routing.
  • RESTful APIs: Experience with making HTTP requests and handling responses.

Development Environment

Ensure your development environment is set up with the following tools:

  • Node.js and npm: Download and install Node.js, which includes npm.

  • Code Editor: We recommend Visual Studio Code with extensions for React and TypeScript.

  • ServiceTitan API Credentials: You'll need access to the following ServiceTitan API credentials:

    • SERVICE_TITAN_CLIENT_ID
    • SERVICE_TITAN_CLIENT_SECRET
    • SERVICE_TITAN_APP_KEY
    • SERVICE_TITAN_TENANT_ID
    • SERVICE_TITAN_BOOKING_PROVIDER_ID

Need Help Obtaining Your Credentials?

If you're unsure how to get these credentials, log in to your ServiceTitan account or reach out to their support team for assistance.

With everything prepared, you're all set to begin building your custom integration!


Project Setup

To kickstart our integration, we'll set up the initial Next.js application and prepare our development environment.

Cloning the Repository

We've prepared a companion repository that contains the base code for this tutorial. This repository is structured to guide you through each phase of the integration.

Key Github Repository Link...

You can find the GitHub Repository ➡️ here ⬅️

Steps to Clone the Repository

  1. Clone the Repository

    Open your terminal and run:

    git clone https://github.com/Aclarify/blog-example-service-titan-bookings.git
    BASH
  2. Navigate to the Project Directory

    cd blog-example-service-titan-bookings
    BASH

Installing Dependencies

With the repository cloned, let's install the necessary dependencies and start the development server.

  1. Install Dependencies: Run the following command in your terminal:

    npm install
    BASH
  2. Start the Development Server: Start the development server with:

    npm run dev
    BASH
  3. View the Application: Open your browser and navigate to http://localhost:3000. You should see the default Next.js welcome page.

Understanding the Repository Structure

Before we start building our booking integration, let's familiarize ourselves with the project's structure and how we'll navigate through the tutorial phases.

Project Overview

Our application is built with:

  • Next.js: For server-side rendering and routing.
  • TypeScript: For type safety and an improved developer experience.
  • Tailwind CSS: For utility-first CSS styling.

Directory Structure

Here's a simplified view of the project's directory structure at the initial phase:

Legend:
* = Key files we'll work with.
 
blog-example-service-titan-bookings/
├── src/
│   └── app/
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx          *
├── public/
│   ├── next.svg
│   └── vercel.svg
├── package.json
├── tailwind.config.ts
├── tsconfig.json
└── ... (other configuration files)
TREE

Key Files Explained:

  • src/app/page.tsx: The main page component where we'll build the booking form.

Each phase of the tutorial corresponds to a branch in the repository. This allows you to:

  • Follow Along Incrementally: Start at the initial phase and build upon it by following the tutorial instructions.
  • Check Your Work: After completing a phase, you can compare your code with the corresponding branch.

Phase Branches Overview

To help you navigate through each phase of the tutorial, we've organized the project repository into the following branches:

Branch NameDescription
phase/0-initial-boilerplateContains the initial boilerplate setup for the Next.js application.
phase/1-ui-form-scaffoldingImplements the user interface for the booking form.
phase/2-create-booking-apiSets up the backend API for handling booking submissions.
phase/3-connect-booking-ui-to-apiConnects the frontend booking form to the backend API.
phase/complete-implementationFinalizes the integration and includes all completed features.

Each branch corresponds to a specific stage in the development process, allowing you to follow along step-by-step:

  1. phase/0-initial-boilerplate: Start here to begin building the user interface for the booking form. This tutorial phase involves creating React components, styling the form using Tailwind CSS, and ensuring the form is responsive and user-friendly.

  2. phase/1-ui-form-scaffolding: This branch guides you through setting up the backend API that will handle form submissions. You'll learn how to create server-side actions in Next.js, define TypeScript types for form data, and implement secure API endpoints to process and store booking information.

  3. phase/2-create-booking-api: In this phase, you'll connect your frontend booking form to the backend API. This involves handling form submissions, managing application state, and ensuring that data flows seamlessly from the UI to the server, with appropriate error handling and user feedback.

  4. phase/3-connect-booking-ui-to-api: Finally, move to this branch to finalize the integration and polish your application. This includes adding additional features, optimizing performance, implementing authentication if necessary, and conducting thorough testing to ensure everything works as intended.

  5. phase/complete-implementation: This branch contains our fully implemented custom Booking application.

By following the branches in order, you can incrementally build and verify each component of your custom integration, ensuring a smooth and organized development experience. Each branch builds upon the previous one, allowing you to track progress and understand how each part contributes to the final implementation.

How to Switch Between Phases

  1. Start at Phase 0: Ensure you're on the initial branch:

    git checkout phase/0-initial-boilerplate
    BASH
  2. Switch to the Next Phase: After completing a phase, switch to the next branch to compare your code:

    git checkout phase/1-ui-form-scaffolding
    BASH
  3. Repeat for Subsequent Phases: Continue this process for each phase of the tutorial.

Preparing for the Integration

Now that you're familiar with the project structure and how to navigate the tutorial, we're ready to begin building our custom booking integration.

Aclarify's Advice

Having a clear understanding of the project setup will make the development process smoother. If you encounter any issues, revisiting this section can help resolve setup-related problems.


Building the Booking Form UI Scaffold

With our project set up and running, it's time to build the user interface for our booking form. This form will capture essential customer information needed to create a booking in ServiceTitan.

Step 1: Cleaning Up the Home Page

First, let's replace the default Next.js content with our booking form component.

Modify page.tsx

Open src/app/page.tsx and replace its content with the following code:

/* src/app/page.tsx */
import { BookingForm } from '@/components/BookingForm'
 
export default function BookingPortal() {
  return (
    <div className="isolate bg-white px-6 py-24 sm:py-32 lg:px-8">
      <div
        className="absolute inset-x-0 top-[-10rem] -z-10 transform-gpu overflow-hidden blur-3xl sm:top-[-20rem]"
        aria-hidden="true"
      >
        <div
          className="relative left-1/2 -z-10 aspect-[1155/678] w-[36.125rem] max-w-none
          -translate-x-1/2 rotate-[30deg] bg-gradient-to-tr from-[#ff80b5]
          to-[#9089fc] opacity-30 sm:left-[calc(50%-40rem)] sm:w-[72.1875rem]"
          style={{
            clipPath:
              'polygon(74.1% 44.1%, 100% 61.6%, 97.5% 26.9%, 85.5% 0.1%, \
80.7% 2%, 72.5% 32.5%, 60.2% 62.4%, 52.4% 68.1%, \
47.5% 58.3%, 45.2% 34.5%, 27.5% 76.7%, 0.1% 64.9%, \
17.9% 100%, 27.6% 76.8%, 76.1% 97.7%, 74.1% 44.1%)',
          }}
        />
      </div>
      <div className="mx-auto max-w-2xl text-center">
        <h2 className="text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
          Create Booking
        </h2>
        <p className="mt-2 text-lg leading-8 text-gray-600 px-10">
          Create a booking with us, your go-to premium HVAC service provider,
          ACME, Inc.!
        </p>
      </div>
      <BookingForm />
    </div>
  )
}
TSX

Explanation:

  • Lines 1-3: We import the BookingForm component, which we'll create next, and define the BookingPortal component.
  • Lines 6-24: We set up the page layout with Tailwind CSS classes for styling, including a background gradient and centered text.
  • Lines 25-31: We add a header and a description to introduce the booking form to users.
  • Line 33: We include the <BookingForm /> component, which will render our booking form.

Step 2: Creating the Booking Form Component

Next, we'll create the BookingForm component that will contain our form fields.

Create the components Directory

In the src directory, create a new folder called components:

src/
├── app/
└── components/
TREE

Create BookingForm.tsx

Inside src/components/, create a new file named BookingForm.tsx and add the following code:

/* src/components/BookingForm.tsx */
import { SubmitButton } from './SubmitButton'
import { Input, InputProps } from './Input'
import { CreateBookingFormDto } from '@/lib/types/booking.type'
 
const formFields: Record<keyof CreateBookingFormDto, InputProps> = {
  firstName: {
    label: 'First Name',
    name: 'firstName',
    autoComplete: 'given-name',
    required: true,
    id: 'firstName',
  },
  lastName: {
    label: 'Last Name',
    name: 'lastName',
    autoComplete: 'family-name',
    required: true,
    id: 'lastName',
  },
  email: {
    label: 'Email',
    name: 'email',
    type: 'email',
    autoComplete: 'email',
    required: true,
    id: 'email',
  },
  phone: {
    label: 'Phone Number',
    name: 'phone',
    type: 'tel',
    autoComplete: 'tel',
    required: true,
    id: 'phone',
  },
  street: {
    label: 'Street Address',
    name: 'street',
    autoComplete: 'street-address',
    required: true,
    id: 'street',
    placeholder: '123 Main St',
  },
  city: {
    label: 'City',
    name: 'city',
    autoComplete: 'address-level2',
    required: true,
    id: 'city',
  },
  state: {
    label: 'State',
    name: 'state',
    autoComplete: 'address-level1',
    required: true,
    id: 'state',
  },
  zip: {
    label: 'ZIP Code',
    name: 'zip',
    autoComplete: 'postal-code',
    required: true,
    id: 'zip',
  },
}
 
export const BookingForm = () => {
  return (
    <form className="mx-auto mt-16 max-w-xl sm:mt-20">
      <div className="grid grid-cols-1 gap-y-6">
        {/* Render form fields */}
        {Object.values(formFields).map((field) => (
          <Input key={field.name} {...field} />
        ))}
      </div>
      <div className="mt-10">
        <SubmitButton />
      </div>
    </form>
  )
}
TSX

Explanation:

  • Lines 6-66: We define formFields, an object mapping each form field to its properties. This will be used to dynamically render input fields.
  • Lines 70-80: The BookingForm component renders a <form> element containing the input fields and a submit button.

Step 3: Creating the Input Component

We'll create a reusable Input component to render each form field.

Create Input.tsx

Inside src/components/, create a new file named Input.tsx and add the following code:

/* src/components/Input.tsx */
import { FC, InputHTMLAttributes, ReactNode } from 'react'
 
export type InputProps = {
  label: ReactNode
} & InputHTMLAttributes<HTMLInputElement>
 
export const Input: FC<InputProps> = ({
  label,
  className,
  name,
  id,
  type = 'text',
  ...props
}) => (
  <div>
    <label
      htmlFor={name}
      className="block text-sm font-semibold leading-6 text-gray-900"
    >
      {label}
    </label>
    <div className="mt-2.5">
      <input
        type={type}
        name={name}
        id={id}
        className={`${className} block w-full rounded-md border-0 px-3.5 py-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder-gray-400 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6`}
        {...props}
      />
    </div>
  </div>
)
TSX

Explanation:

  • Lines 4-6: We define the InputProps type, extending standard input attributes and adding a label prop.
  • Lines 16-32: The Input component renders a <label> and an <input> element with appropriate styling and props.

Step 4: Creating the Submit Button Component

Next, we'll create a SubmitButton component for form submission.

Create SubmitButton.tsx

Inside src/components/, create a new file named SubmitButton.tsx and add the following code:

/* src/components/SubmitButton.tsx */
export const SubmitButton = () => {
  return (
    <button
      type="submit"
      className="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-600"
    >
      Create Booking
    </button>
  )
}
TSX

Explanation:

  • Lines 4-9: The SubmitButton component renders a styled <button> element for form submission.

Step 5: Defining TypeScript Types

We'll define the types for our booking form data.

Create booking.type.ts

Create a new directory src/lib/types/ and inside it, create a file named booking.type.ts with the following content:

/* src/lib/types/booking.type.ts */
export type CreateBookingFormDto = {
  firstName: string
  lastName: string
  email: string
  phone: string
  street: string
  city: string
  state: string
  zip: string
}
TS

Explanation:

  • We define the CreateBookingFormDto type, which represents the structure of our booking form data.

Step 6: Testing the Form

Now that we've set up all the components, your application should display the booking form when you navigate to http://localhost:3000.

Visual Check:

Initial Booking Form Scaffold

Keep in mind...

At this point, the form doesn't have any functionality; we'll handle form submission and integrate with the API in the next sections.


Creating the Backend API for Booking

With the frontend form in place, it's time to create the backend API that will handle booking submissions. This API will process the form data and communicate with ServiceTitan's API to create a booking.

Step 1: Setting Up the Booking Action

We'll create a server-side action in Next.js to process the booking form data securely.

Create createBooking.action.ts

Create a new directory src/lib/actions/ and inside it, create a file named createBooking.action.ts with the following content:

/* src/lib/actions/createBooking.action.ts */
'use server'
 
import { nanoid } from 'nanoid'
import {
  CreateBookingFormDto,
  CreateBookingOutboundDto,
} from '../types/booking.type'
import { makeRequest } from '../utils/makeSTRequest.util'
import { serviceTitanConfig } from '../config/serviceTitan.config'
 
export async function createBooking(formData: FormData) {
  try {
    // Extract and validate form data
    const fields = [
      'firstName',
      'lastName',
      'email',
      'phone',
      'street',
      'city',
      'state',
      'zip',
    ]
 
    const bookingInboundDto = {} as CreateBookingFormDto
 
    for (const field of fields) {
      const value = formData.get(field)?.toString().trim()
      if (!value) {
        throw new Error(`Field "${field}" is required.`)
      }
      bookingInboundDto[field as keyof CreateBookingFormDto] = value
    }
 
    // Map form data to the structure expected by ServiceTitan's API
    const bookingOutboundDto: CreateBookingOutboundDto = {
      source: 'Website',
      summary: 'New booking request',
      isFirstTimeClient: true,
      externalId: nanoid(),
      name: `${bookingInboundDto.firstName} ${bookingInboundDto.lastName}`,
      address: {
        street: bookingInboundDto.street,
        city: bookingInboundDto.city,
        state: bookingInboundDto.state,
        zip: bookingInboundDto.zip,
        country: 'USA',
      },
      contacts: [
        { type: 'Email', value: bookingInboundDto.email },
        { type: 'Phone', value: bookingInboundDto.phone },
      ],
    }
 
    // Make API request to create booking
    await makeRequest({
      url: `https://api.servicetitan.io/crm/v2/tenant/${serviceTitanConfig.tenantId}/booking-provider/${serviceTitanConfig.bookingProviderId}/bookings`,
      method: 'POST',
      body: bookingOutboundDto,
    })
 
    // Return a success message
    return {
      success: true,
      message: 'Booking created successfully.',
    }
  } catch (error: any) {
    console.error('Error creating booking:', error)
 
    // Return the error message to the client
    return {
      success: false,
      message: error.message || 'An error occurred while creating the booking.',
    }
  }
}
TS

Explanation:

  • Lines 15-34: Loops through required fields, extracts, and validates form data from the FormData object.
  • Lines 37-54: Transforms the inbound form data into the structure required by ServiceTitan's API.
  • Lines 57-61: Uses the makeRequest utility to send a POST request to ServiceTitan's API to create a booking.
  • Lines 68-76: Catches and logs any errors, returning a meaningful message to the client.

Step 2: Configuring ServiceTitan Settings

We'll create a configuration file to manage our ServiceTitan credentials and settings securely.

Create serviceTitan.config.ts

Create a new directory src/lib/config/ and inside it, create a file named serviceTitan.config.ts with the following content:

/* src/lib/config/serviceTitan.config.ts */
const bootstrap = () => {
  const requiredEnvVars = [
    'SERVICE_TITAN_APP_KEY',
    'SERVICE_TITAN_TENANT_ID',
    'SERVICE_TITAN_BOOKING_PROVIDER_ID',
    'SERVICE_TITAN_CLIENT_ID',
    'SERVICE_TITAN_CLIENT_SECRET',
  ]
 
  requiredEnvVars.forEach((envVar) => {
    if (!process.env[envVar]) {
      throw new Error(`Missing required environment variable: ${envVar}`)
    }
  })
}
 
bootstrap()
 
export const serviceTitanConfig = {
  tenantId: process.env.SERVICE_TITAN_TENANT_ID as string,
  appKey: process.env.SERVICE_TITAN_APP_KEY as string,
  bookingProviderId: process.env.SERVICE_TITAN_BOOKING_PROVIDER_ID as string,
  clientId: process.env.SERVICE_TITAN_CLIENT_ID as string,
  clientSecret: process.env.SERVICE_TITAN_CLIENT_SECRET as string,
}
TS

Explanation:

  • Lines 2-18: An immediately invoked function expression (IIFE) checks for the presence of required environment variables. If any are missing, it throws an error to prevent the application from running without necessary credentials.
  • Lines 20-26: Exports a configuration object containing all ServiceTitan-related credentials, ensuring they are accessible throughout the application.

Step 3: Defining TypeScript Types for Outbound Data

We need to define the structure of the data we'll send to ServiceTitan's API.

Update booking.type.ts

Inside src/lib/types/, update the existing booking.type.ts file or create it if it doesn't exist, and add the following content:

/* src/lib/types/booking.type.ts */
export type Address = {
  street: string
  city: string
  state: string
  zip: string
  country: string
}
 
export type Contact = {
  type: 'Phone' | 'Email'
  value: string
}
 
export interface CreateBookingOutboundDto {
  source: string
  name: string
  summary: string
  isFirstTimeClient: boolean
  externalId: string
  address: Address
  contacts: Contact[]
}
 
export type CreateBookingFormDto = {
  firstName: string
  lastName: string
  email: string
  phone: string
  street: string
  city: string
  state: string
  zip: string
}
TS

Explanation:

  • Lines 2-8: Defines the structure for address information.
  • Lines 10-13: Defines the structure for contact information, specifying the type of contact (Phone or Email).
  • Lines 15-23: Represents the data structure required by ServiceTitan's API to create a booking.

Step 4: Creating Utility Functions for Authentication and API Requests

We'll create utility functions to handle authentication with ServiceTitan and make API requests.

Create authenticateST.util.ts

Create a new directory src/lib/utils/ and inside it, create a file named authenticateST.util.ts with the following content:

/* src/lib/utils/authenticateST.util.ts */
import { serviceTitanConfig } from '../config/serviceTitan.config'
 
export const authenticateServiceTitan = async (): Promise<string> => {
  const response = await fetch('https://auth.servicetitan.io/connect/token', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      client_id: serviceTitanConfig.clientId,
      client_secret: serviceTitanConfig.clientSecret,
      grant_type: 'client_credentials',
    }).toString(),
  })
 
  if (!response.ok) {
    const errorData = await response.json()
    console.error('Authentication failed:', errorData)
    throw new Error('Failed to authenticate with ServiceTitan API.')
  }
 
  const data = await response.json()
  return data.access_token
}
TS

Explanation:

  • authenticateServiceTitan Function: Authenticates with ServiceTitan's API using client credentials and retrieves an access token.
    • Lines 5-15: Sends a POST request to the authentication endpoint with required credentials.
    • Lines 17-21: Logs and throws an error if authentication fails.
    • Line 24: Returns the access token upon successful authentication.

Create makeSTRequest.util.ts

Inside src/lib/utils/, create a file named makeSTRequest.util.ts with the following content:

/* src/lib/utils/makeSTRequest.util.ts */
import { serviceTitanConfig } from '../config/serviceTitan.config'
import { authenticateServiceTitan } from './authenticateST.util'
 
interface MakeRequestOptions {
  url: string
  method: 'GET' | 'POST' | 'PUT' | 'DELETE'
  body?: unknown
}
 
export const makeRequest = async <T = unknown>(
  options: MakeRequestOptions
): Promise<T> => {
  const accessToken = await authenticateServiceTitan()
 
  const { url, method, body } = options
 
  const response = await fetch(url, {
    method,
    headers: {
      'Content-Type': 'application/json',
      'ST-App-Key': serviceTitanConfig.appKey,
      Authorization: `Bearer ${accessToken}`,
    },
    body: body ? JSON.stringify(body) : undefined,
  })
 
  if (!response.ok) {
    const errorData = await response.json()
    console.error(`API request failed: ${response.status}`, errorData)
    throw new Error(
      `ServiceTitan API request failed with status ${response.status}`
    )
  }
 
  const data = await response.json()
  return data as T
}
TS

Explanation:

  • makeRequest Function: A generic function to make authenticated API requests to ServiceTitan.
    • Line 14: Retrieves an access token using authenticateServiceTitan.
    • Lines 20-24: Sets up headers with the access token and app key, and includes the request body if provided.
    • Lines 28-34: Logs and throws an error if the API request fails.
    • Line 37: Returns the parsed JSON response from the API.

Step 5: Adding Environment Variables

Ensure you have the required environment variables set up in your .env.local file at the root of your project:

SERVICE_TITAN_APP_KEY=your_service_titan_app_key
SERVICE_TITAN_TENANT_ID=your_service_titan_tenant_id
SERVICE_TITAN_BOOKING_PROVIDER_ID=your_service_titan_booking_provider_id
SERVICE_TITAN_CLIENT_ID=your_service_titan_client_id
SERVICE_TITAN_CLIENT_SECRET=your_service_titan_client_secret
BASH

Explanation:

  • .env.local File: Stores sensitive information like API keys and secrets. Ensure this file is included in your .gitignore to prevent it from being committed to version control.
  • Variables Required:
    • SERVICE_TITAN_APP_KEY
    • SERVICE_TITAN_TENANT_ID
    • SERVICE_TITAN_BOOKING_PROVIDER_ID
    • SERVICE_TITAN_CLIENT_ID
    • SERVICE_TITAN_CLIENT_SECRET

Connecting the Frontend with the Backend

Now that we've built both the frontend form and the backend API, it's time to connect them. We'll update our form to submit data to our backend action and enhance the user experience with responsive feedback.

Step 1: Integrate the Server Action into the Form

We'll modify our BookingForm component to handle form submission using server actions, ensuring secure processing of form data.

Update BookingForm.tsx

Open src/components/BookingForm.tsx and make the following changes:

/* src/components/BookingForm.tsx */
'use client'
 
import { useRef, useState } from 'react'
import { toast, Toaster } from 'react-hot-toast'
import { createBooking } from '@/lib/actions/createBooking.action'
import { SubmitButton } from './SubmitButton'
import { Input, InputProps } from './Input'
import { CreateBookingFormDto } from '@/lib/types/booking.type'
 
const formFields: Record<keyof CreateBookingFormDto, InputProps> = {
  // ... Previous form fields
}
 
export const BookingForm = () => {
  const [serverActionResult, setServerActionResult] = useState<{
    success: boolean
    message: string
  } | null>(null)
 
  const formRef = useRef<HTMLFormElement>(null)
 
  const handleCreateBooking = async (formData: FormData) => {
    const result = await createBooking(formData)
    setServerActionResult(result)
    if (result.success) {
      // Show success toast
      toast.success(result.message)
      // Reset the form
      formRef?.current?.reset()
    } else {
      // Show error toast
      toast.error(result.message)
    }
  }
 
  return (
    <>
      <Toaster />
      <form
        ref={formRef}
        action={handleCreateBooking}
        className="mx-auto mt-16 max-w-xl sm:mt-20"
      >
        <div className="grid grid-cols-1 gap-y-6">
          {/* Render form fields */}
          {Object.values(formFields).map((field) => (
            <Input key={field.name} {...field} />
          ))}
        </div>
        {/* Display Success or Error Message */}
        {serverActionResult && (
          <p
            className={`mt-4 text-sm ${
              serverActionResult.success ? 'text-green-600' : 'text-red-600'
            }`}
          >
            {serverActionResult.message}
          </p>
        )}
        {/* Submit Button */}
        <div className="mt-10">
          <SubmitButton />
        </div>
      </form>
    </>
  )
}
TSX

Explanation:

  • Line 2: Ensure we have use client enabled for client-side rendering since our form leverages client-side state management like useState.
  • Lines 16-21: Initialize state for serverActionResult and create a formRef using useRef to reset the form after submission.
  • Lines 23-35: Define the handleCreateBooking function, which calls the createBooking action, updates the state with the result, shows toast notifications, and resets the form on success.
  • Line 39: Update the JSX to include the <Toaster /> component for toast notifications
  • Line 41-42: Attach the formRef to the <form> element and set the action prop to handleCreateBooking
  • Line 51-60: Conditionally display success or error messages.

Step 2: Making the Submit Button Stateful

We'll enhance the SubmitButton component to reflect the form submission status, providing better feedback to the user.

Update SubmitButton.tsx

Open src/components/SubmitButton.tsx and modify it as follows:

/* src/components/SubmitButton.tsx */
'use client'
 
import { useFormStatus } from 'react-dom'
 
export const SubmitButton = () => {
  const { pending } = useFormStatus()
 
  return (
    <button
      type="submit"
      disabled={pending}
      className="block w-full rounded-md bg-indigo-600 px-3.5 py-2.5 text-center text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 disabled:opacity-50 focus:outline-none focus:ring-2 focus:ring-indigo-600"
    >
      {pending ? 'Submitting...' : 'Create Booking'}
    </button>
  )
}
TSX

Explanation:

  • Line 2: Ensure we have use client enabled for client-side rendering since our form leverages client-side state management like useState.
  • Line 7: Destructure pending from useFormStatus() to track the form submission state.
  • Line 12: Disable the submit button when the form is submitting by setting disabled={pending}.
  • Line 15: Change the button text to "Submitting..." when the form is in the pending state.

Step 3: Adding Toast Notifications for User Feedback

We'll use react-hot-toast to display toast notifications for success and error messages, enhancing the user experience.

Install react-hot-toast

First, install the react-hot-toast package:

npm install react-hot-toast
BASH

Update package.json

Your package.json should now include react-hot-toast in the dependencies:

{
  // ...
  "dependencies": {
    // ...
    "react-hot-toast": "^2.4.1"
  }
}
JSON

Explanation:

  • Installation: This command adds react-hot-toast to your project, allowing you to use toast notifications.

Update BookingForm.tsx (Continued)

We have already integrated react-hot-toast in BookingForm.tsx during Step 1 by importing toast and Toaster, and invoking toast.success and toast.error within the handleCreateBooking function.

Note: Ensure that you have added the <Toaster /> component inside the return statement to render the toast notifications.

Step 4: Finalizing the Integration

With these changes, your form now securely submits data to the backend, provides responsive feedback to users, and enhances the overall user experience.


Testing the Integration

With both the frontend and backend set up and connected, it's time to test the entire booking process. In this section, we'll run the application, submit a test booking, verify it in ServiceTitan, and address common issues that might arise.

Step 1: Running the Application

First, let's start the development server to ensure everything is functioning as expected.

Start the Development Server

Open your terminal, navigate to your project directory, and run the following command:

npm run dev
BASH

Explanation:

  • This command starts the Next.js development server, making your application accessible at http://localhost:3000.

Access the Application

Open your preferred web browser and navigate to: http://localhost:3000

You should see the booking form we created earlier.

Step 2: Submitting a Test Booking

Now, let's simulate a user booking by filling out the form.

Fill Out the Form

Enter the following test data into the form fields:

  • First Name: Jane
  • Last Name: Smith
  • Email: jane.smith@example.com
  • Phone Number: (555) 123-4567
  • Street Address: 456 Elm Street
  • City: Springfield
  • State: IL
  • ZIP Code: 62704

Form Filled Out With Test Data:

Form Filled Out With Test Data

Use Test Data

Ensure you're using test data to prevent any unintended real bookings.

Submit the Form

Click the Create Booking button at the bottom of the form.

Observe the Response

  • Success Message: If the booking was created successfully, a message like "Booking created successfully." will appear.
  • Error Message: If there was an issue, an error message will display with details.
  • The form status message will appear below the form, styled in green for success or red for errors.

Successful Form Submission:

Form Submission Success

Failed Form Submission:

Form Submission Success

Step 3: Verifying the Booking in ServiceTitan

To confirm that the booking was successfully created, we'll check your ServiceTitan account.

Log into ServiceTitan

Locate the New Booking

  • Go to the Bookings or Appointments section in your ServiceTitan dashboard.
  • Search for the booking under the name "Jane Smith."

Verify Booking Details

  • Ensure that all the information matches the data you entered in the form.
  • Confirm that the booking status and other details are correct.

Note: It might take a few moments for the booking to appear in ServiceTitan due to processing times.

Step 4: Troubleshooting Common Issues

If you encounter any problems during testing, consider the following troubleshooting tips.

Common Issues and Solutions

  • Issue: No success message after form submission.
    • Solution: Check the browser console for errors. Ensure that the backend server is running and accessible.
  • Issue: Booking doesn't appear in ServiceTitan.
    • Solution: Wait a few minutes and refresh the page. Verify that your API credentials are correct and that you have the necessary permissions.
  • Issue: Error messages about authentication or authorization.
    • Solution: Double-check your environment variables in .env.local and ensure all required variables are set correctly.

Keep in Mind

Ensure that your ServiceTitan API credentials are valid and that your account has permission to create bookings via the API.

Next Steps

Congratulations! You've successfully tested the integration between your Next.js application and ServiceTitan's API. This foundational setup can be expanded to include additional features like scheduling options, service selections, and customer authentication.

By integrating with ServiceTitan, you're streamlining your booking process and enhancing the customer experience. This kind of seamless integration is crucial for modern businesses looking to stay competitive and meet customer expectations.


Conclusion: Empowering Your Service Business with Modern Integration

Throughout this tutorial, we've journeyed through the process of integrating a Next.js application with ServiceTitan's API to create a seamless booking experience. Let's recap the key milestones we've achieved:

  • Project Setup: Cloned the starter repository and set up the development environment.
  • Building the Booking Form UI: Created a responsive booking form using React components and Tailwind CSS.
  • Creating the Backend API:
    • Developed server-side actions to securely process form submissions.
    • Configured authentication with ServiceTitan's API.
  • Connecting Frontend and Backend:
    • Integrated the frontend form with the backend action.
    • Enhanced user experience with real-time feedback using react-hot-toast.
  • Testing the Integration:
    • Successfully submitted test bookings and verified them in ServiceTitan.
    • Addressed common issues to ensure a robust implementation.

By following these steps, you've built a functional booking system that not only streamlines your operations but also enhances customer engagement. This integration serves as a solid foundation for further enhancements and scaling your service business.

Next Steps

With the groundwork laid, consider expanding your application with additional features:

  • Automate Customer Support Workflows: Upon bookings, send automated confirmations, reminders, and advance payment collection requests to customers.
  • Scheduling Options: Allow customers to select preferred service dates and times.
  • Service Selection: Enable customers to choose from a list of services you offer.
  • User Authentication: Implement account creation and login functionality for returning customers.
  • Data Analytics: Integrate tools to analyze booking trends and customer behavior.

By continuing to innovate and adapt, you'll stay ahead in a competitive market, offering unparalleled service and convenience to your customers.


Partner with Aclarify for Success

While you've made significant progress, taking your service business to the next level often requires expert guidance and resources. That's where Aclarify comes in.

Why Choose Aclarify?

  • Deep Expertise: Our team specializes in integrating ServiceTitan with cutting-edge web technologies, ensuring seamless and efficient solutions.
  • Customized Solutions: We tailor our services to meet your specific business needs, providing a bespoke approach rather than off-the-shelf products.
  • Dedicated Support: From initial consultation to ongoing maintenance, we're committed to your success every step of the way.

Let's Transform Your Business Together

Imagine a fully integrated system where bookings, customer interactions, and service management are all streamlined and automated. Envision empowering your team with tools that boost productivity and delighting your customers with exceptional service experiences.

At Aclarify, we're passionate about making this a reality for you.

Ready to Accelerate Your Digital Transformation?

Unlock your service business's full potential with Aclarify's expert integration and development solutions. Contact us today to start your journey towards innovation and growth.

More articles

Navigating Legacy System Modernization: A Step-by-Step Guide

Embark on a journey of transformation and growth by modernizing your outdated systems. This comprehensive guide provides actionable insights and expert advice to navigate the complexities of system modernization, tailored for businesses aiming for innovation and efficiency.

Read more

Building Your MVP: A Comprehensive Guide for Software Development Teams

Discover the critical steps for developing a Minimum Viable Product (MVP) that aligns with user needs and market demands, and learn how Aclarify can assist in bringing your software product to market efficiently.

Read more

Tell us about your project

Our offices

  • New Hampshire
    195 Carley Rd
    Peterborough, New Hampshire
    03458 USA
  • California
    1155 5th St, 406
    Oakland, California
    02607 USA