41

I'm working on a project with a Node.js, Express.js & TypeScript backend (REST API) and a React, Redux & TypeScript frontend.

In the backend I have created some types for example:

models/Product.ts

export type Product = {
    title: string
    description: string
    price: number
}

So the REST API is sending a response like the following:

{
  "data": [
     {"title": "Shoe", "Description": "This is a shoe.", "price": 20},
     {...}
   ] // inside here all the Products[]
}

On the client I want to cast the data to the type Product array. But since the frontend and backend are different code bases and separated, I still want to take the advantage of using types on the frontend. But I want to achieve this without duplicating code. So I don't want to update 2 places when modifying or adding models.

Does someone know what is the best way to share types between client and server?

I was thinking of something like creating an endpoint on the backend which the client can hit, then it writes all the models in a single file for example models.ts on the client. So I think I need to loop through every file inside /models/ on the backend then parsing it in a new file which is written on the client as models.ts. But is this really a good way? Does someone know a better way of achieving this structure?

1

4 Answers 4

50

You can use TypeScript path mapping.

Example from a project I'm the author of:
Backend defined types inside SampleTypes.ts are reused in the client project to avoid duplicating the code.

client/tsconfig.json:

{
  "compilerOptions": {
    "paths": { "@backend/*": ["../server/src/api/*"], },
  }
}

../server/src/api/ -> https://github.com/winwiz1/crisp-react/tree/master/server/src/api

client/....ts:

import { SampleRequest } from "@backend/types/SampleTypes";
8
  • Nice, that worked like a charm for me. Much better than just copy paste it everytime there is a change.
    – Raqha
    Apr 23, 2022 at 23:06
  • 6
    This works, but does mean the api and client src trees can't be used as isolated docker contexts since a context of client can't look at ../server. (For now I just moved context up a level, but that's not ideal?)
    – plswork04
    Apr 25, 2022 at 23:28
  • 1
    @plswork04 Suggest to use Docker multi-staged build to cut image size and improve security by reducing the attack surface. Stages: (1) Copy client and server code, install client dependencies and build client. (2) Start afresh, copy server code only, install server dependencies and build server. (3) Create the final image by starting again afresh with fresh OS image, install server run-time dependencies only (no dev dependencies including TypeScript, no source code), copy from previous stages the build artifacts (client's - script bundles and .html files, server's - transpiled JS files).
    – winwiz1
    Apr 27, 2022 at 9:15
  • 3
    with create-react-app it's impossible to include outside the client directory. maybe the server could include stuff in the client but i've seen the tsc command then build both client and server projects...
    – dcsan
    May 2, 2022 at 22:23
  • 1
    also remember your paths are relative to 'baseUrl': './src'. but with this setting tsc creates build/client and build/server/src/...js ie including anything from client also compiles the whole client dir
    – dcsan
    May 2, 2022 at 22:36
6

You are essentially looking to share code (in your case type definitions) between multiple packages. Yarn has developed workspaces for this purpose, and it does not rely on tsconfig/Typescript as the other answer does.

It's a dive into a rabbit hole, with a bit of work of your Yarn configuration and possibly the use of tools like Lerna. It does make sense however, when you have tightly coupled packages that share types, but perhaps also validation logic.

1
  • On top of this, I would say this is basically the premise of a monorepo. lerna is also now under nrwl (or nx) who maintain a good set of tools for monorepos. It is indeed a rabbit hole and requires a significant time investment and is only relevant based on your team's priorities. Jun 30, 2023 at 18:55
0

You can structure your Node.js code in modules where each module's types are in type.ts file.

For example:

src/modules/devices/types.ts

export interface Route {
  timestamp: string
  name: string
  geojsonString: string
}

export type AddRouteBodyParams = Pick<Route, 'name' | 'geojsonString'>

export interface DeleteRouteBodyParams {
  deviceId: string
  timestamp: string
}

export interface EditRouteBodyParams {
  deviceId: string
  timestamp: string
  name: string
  geojsonString: string
}

src/modules/devices/controllerDevice.ts

import type { Request, Response } from 'express'
import type { ParsedQs } from 'qs'

import type { ResponseError, ResponseSuccess } from '../../api-types'

import { editRoute } from './serviceRoutes'
import type { EditRouteBodyParams } from './types'

export const deviceController = {
  editRoute: async (
    req: Request<any, any, EditRouteBodyParams, ParsedQs, Record<string, any>>,
    res: Response<ResponseSuccess | ResponseError>
  ) => {
    const editResponse = await editRoute({ ...req.body })
      if (editResponse instanceof Error) {
        return res.status(500).json({ error: editResponse.message })
      }
      return res.send({ message: `Route with name: ${req.body.name} was updated` })
  },
} as const

Then, you can export all api types into a single file and copy it into front-end project by running npm command:

"scripts": {
    "generate-types": "tsc -p tsconfig-export.json && cp ./dist/export-api-types.d.ts ../client_map_app/src/types"
}

You can check more details here.

0

Make two same files for shared types on BE and FE. Do an OS Hardlink between them.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.