Share types between client and server
Asked Answered
Y

4

43

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?

Yogini answered 27/11, 2020 at 23:55 Comment(1)
This could be answered here #66529567Farlie
S
51

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";
Sermonize answered 28/11, 2020 at 3:16 Comment(8)
Nice, that worked like a charm for me. Much better than just copy paste it everytime there is a change.Motivate
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?)Helldiver
@Raqha Glad it worked for you.Sermonize
@Helldiver 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).Sermonize
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...Aesop
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 dirAesop
So I tried this and it seems to compile both the client and server, but my react-native client throws an Unresolved module error - it can't find the module I'm referencing. I'm using Expo to do my builds and testingAvera
@IraKlein If TypeScript compiler (TSC) happily transpiles .ts file into .js file but at run-time the JS code cannot find the referenced (I assume the imported) module, then the two most likely issues are: 1. The bundler doesn't support Path Mapping and cannot understand the import. 2. The bundler/builder has not been configured to support Path Mapping so it doesn't process the import correctly. I'm not familiar enough with Expo but it appears it can use either TSC or Metro to transpile TS into JS so the problem could potentially be with Metro. E.g. transpiling and/or bundling incorrectly.Sermonize
D
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.

Doily answered 19/11, 2022 at 14:38 Comment(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.Jus
W
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.

Wallaby answered 6/8, 2023 at 18:42 Comment(0)
A
0

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

Adalard answered 28/11, 2023 at 22:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.