Eliminate hard coded paths in front-end development!👊💥
Tags: typescript, nextjs, react
Published: 2023-10-05 03:08:38

---
title: Eliminate hard coded paths in front-end development!👊💥
published: true
description: 
tags: typescript, nextjs, react
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2023-09-20 23:15 +0000
---

I've released an npm library based on the content of this article! Please try it out!
https://github.com/koyablue/path-kanri

***

I'm sure that many of you have seen something like this before.
```typescript
import { useRouter } from 'next/router'

const ExampleComponent = () => {
  const router = useRouter();
  ...
  const randomFunc = () => {
    ...
   router.push(`/example/${exampleId}/${slug}`); // <-THIS!!!!!!!!!!!
  }
  
  return(
    <div>
      ...
    </div>
  );
};
```
Yes, hard coded paths.
We usually avoid hard coding any value(not just paths)as much as possible. But we all make compromises. I think there are many opportunities to see something like the above.
So this time, I tried to think of a way to manage all the paths in one place and call them with functions.
Like this:
```typescript
// if there is a path "/example/{exampleId}/{slug}"

// this function returns "/example/1/abcd"
getPath('example', { exampleId: 1, slug: 'abcd' })
```

And also like this:
```typescript
// 'https://example.com/example/1/abc/?page=1&type=fire'
// If the base url isn't registered: '/example/1/abc/?page=1&type=fire'
getFullPath('example', { exampleId: 1, slug: 'abc' }, { page: '1', type: 'fire' });
```

# Tech stack
- TypeScript

# Step-by-step implementation

## Types
```typescript
export type PathParams = { [key: string]: string | number };

// union type of object value
export type ValueOf<T> = T[keyof T];
```

## Functions

```typescript
// types
import { ValueOf, PathParams } from '../types/index';

// constants
import { missingRequiredParametersMsg, invalidParametersMsg } from './constants';

/**
 *
 *
 * @template TPathNameAndUriMap
 * @param {TPathNameAndUriMap} pathNameAndUriMap ex) { name: '/example/{a}/{b}', ... }
 * @param {string} [baseUrl] ex) 'https://api.example.com'
 * @return {*} functions
 */
const pathManager = <TPathNameAndUriMap extends { [s: string]: unknown; }>(
  pathNameAndUriMap: TPathNameAndUriMap,
  baseUrl: string = '',
) => {
  type PathName = keyof typeof pathNameAndUriMap;
  type Uri = ValueOf<typeof pathNameAndUriMap>;

  /**
   * Returns array of parameter name
   * ex) getParamNamesFromRawUri('/example/{exampleId}/{slug}') -> ['exampleId', 'slug']
   *
   * @param {Uri} rawUri
   * @return {*}  {string[]}
   */
  const getParamNamesFromRawUri = (rawUri: Uri): string[] => {
    const rawUriStr = String(rawUri);
    const paramNamesWithBrackets = rawUriStr.match(/{.+?}/g);
    if (paramNamesWithBrackets === null) return [];

    return paramNamesWithBrackets.map((paramNameWithBrackets) => paramNameWithBrackets.replace(/{|}/g, ''));
  };
  type ParamNames = ReturnType<typeof getParamNamesFromRawUri>;

  /**
   * Validate number of parameters and parameter names
   *
   * @param {PathParams} params
   * @param {ParamNames} paramNames
   * @param {PathName} pathName
   * @param {TPathNameAndUriMap[keyof TPathNameAndUriMap]} rawUri
   */
  const validateParams = (
    params: PathParams,
    paramNames: ParamNames,
    pathName: PathName,
    rawUri: TPathNameAndUriMap[keyof TPathNameAndUriMap],
  ) => {
    const paramsKeys = Object.keys(params);

    if (paramsKeys.length !== paramNames.length) {
      throw new Error(missingRequiredParametersMsg(String(pathName), String(rawUri)));
    }

    if (!paramsKeys.every((paramKey) => paramNames.includes(paramKey))) {
      throw new Error(invalidParametersMsg(String(pathName), String(rawUri)));
    }
  };

  /**
   * Generate query params string from object
   * ex) generateQueryParamsStr({ page: '1', type: 'fire' }) => 'page=1&type=fire'
   *
   * @param {Record<string, string>} paramsObj
   * @return {*}  {string}
   */
  const generateQueryParamsStr = (paramsObj: Record<string, string>): string => (
    new URLSearchParams(paramsObj).toString()
  );

  /**
   * Returns full path
   *
   * @param {string} path
   * @return {*}  {string}
   */
  const withBaseUrl = (path: string): string => `${baseUrl}${path}`;

  /**
   * Returns a path with query parameters
   *
   * @param {string} path
   * @param {Record<string, string>} queryParams
   * @return {*}  {string}
   */
  const withQueryParams = (path: string, queryParams: Record<string, string>): string => `${path}/?${generateQueryParamsStr(queryParams)}`;

  /**
   * Get path
   *
   * getActualUri('example', { exampleId: 1, slug: 'abcd' }) -> '/example/1/abcd'
   *
   * @param {PathName} pathName
   * @param {PathParams} [params]
   * @return {*}  {string}
   */
  const getPath = (
    pathName: PathName,
    params?: PathParams,
    queryParams?: Record<string, string>,
  ): string => {
    const rawUri = pathNameAndUriMap[pathName]; // ex) '/example/{exampleId}/{slug}'

    const paramNames = getParamNamesFromRawUri(rawUri); // ex) ['exampleId', 'slug']
    const rawUriStr = String(rawUri); // This is just for type conversion

    // Return if the path doesn't contain any parameter placeholder
    if (!paramNames.length) return withBaseUrl(rawUriStr);

    // The path contains parameter placeholders but params doesn't provided as the 2nd argument
    if (!params) {
      throw new Error(missingRequiredParametersMsg(String(pathName), rawUriStr));
    }

    // Throw error if the params are invalid
    validateParams(params, paramNames, pathName, rawUri);

    // Fill the parameter placeholder with params
    // '/example/{exampleId}/{slug}' -> '/example/1/abcd'
    let pathToReturn = rawUriStr;
    paramNames.forEach((paramName) => {
      pathToReturn = pathToReturn.replace(`{${paramName}}`, String(params[paramName]));
    });

    // ex) 'https://example.com/example/1/abcd/?page=1&type=fire'
    if (queryParams) return withQueryParams(pathToReturn, queryParams);

    return pathToReturn;
  };

  const getFullPath = (
    pathName: PathName,
    params?: PathParams,
    queryParams?: Record<string, string>,
  ): string => withBaseUrl(getPath(pathName, params, queryParams));

  return { getPath, getFullPath } as const;
};

export default pathManager;
```

- constants
```typescript
const nameAndUri = (pathName: string, rawUri: string) => `[NAME: ${pathName}][URI: ${rawUri}]`;

export const missingRequiredParametersMsg = (pathName: string, rawUri: string) => `Missing required parameters for ${nameAndUri(pathName, rawUri)}.`;

export const invalidParametersMsg = (pathName: string, rawUri: string) => `Given parameters are not valid for ${nameAndUri(pathName, rawUri)}.`;
```

# How to use it
in .env:
```
APP_BASE_URL=https://example.com
API_BASE_URL=https://api.example.com
```

make util function as well:
```typescript
export const getAppBaseUrl = (): string => process.env.APP_BASE_URL || '';

export const getApiBaseUrl = (): string => process.env.API_BASE_URL || '';
```

Then register paths.
```typescript
import pathManager from 'path-kanri';
import { getApiBaseUrl } from '../path/to/utils'; 

const {
  getPath: getApiRoute,
  getFullPath: getApiRouteFull,
} = pathManager({
  register: '/register',
  users: '/users',
  userProfile: '/users/{userId}',
  userPosts: '/users/{userId}/posts',
  userPost: '/users/{userId}/posts/{postId}',
}, getApiBaseUrl());

export { getApiRoute, getApiRouteFull };
```

```typescript
import pathManager from 'path-kanri';
import { getAppBaseUrl } from '../path/to/utils'; 

const {
  getPath: getWebRoute,
  getFullPath: getWebRouteFull,
} = pathManager({
  home: '/dashboard',
  userProfile: '/users/{userId}',
  userPosts: '/users/{userId}/posts',
  userPost: '/users/{userId}/posts/{postId}',
}, getApiBaseUrl());

export { getWebRoute, getWebRouteFull };
```

Use it in redirect logics.
```typescript
import { useRouter } from 'next/router';
import { getWebRoute } from '../routes/web';

const ExampleComponent = () => {
  const router = useRouter();
  
  const { getPath } = usePaths();
  
  const randomFunc = () => {
    ...
    const userId = user.id;
    const postId = post.id;
    router.push(getWebRoute('userPost', { userId, postId }));
  };
  
  return(
    <div>
      ...
    </div>
  );
};
```

in API request logics as well.
```typescript
import axios, { AxiosInstance } from 'axios';
import { apiBaseUrl } from '../const/api';
import { getApiEndpoint } from '../routes/api';
import { UserRegistrationRequest } from '../../types';

/**
 * Base axios instance for API calls
 *
 * @param {string} token
 * @return {*}  {AxiosInstance}
 */
export const axiosBase = (token: string = ''): AxiosInstance => {
  const bearerConfig = token ? `Bearer ${token}` : '';

  return axios.create({
    baseURL: apiBaseUrl,
    headers: {
      accept: 'application/json',
      'Content-Type': 'application/json',
      Authorization: bearerConfig,
    },
  });
};

/**
 * User registration API call
 *
 * @param {UserRegistrationRequest} values
 * @return {*}  {Promise<void>}
 */
export const registerUser = async (
  values: UserRegistrationRequest,
): Promise<void> => {
  const res = await axiosBase().post(getApiEndpoint('register'), values);

  return res.data;
};
```

Of course auto type completion is available in the editors.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aozyq7400xtpfsp003ki.png) 

---

That's all. I hope this article was helpful for you.

How do you usually manage URLs and paths? If you have better ways, let me know in the comments!

And if you found this article helpful, please share it on social media!

---

(Original article I wrote in Japanese: https://zenn.dev/koyabluetech/articles/b5c23cbd78cc64)