Skip to content

Shared Types for Frontend and Backend #45

@bhuelsman

Description

@bhuelsman

Description
Right now, types like Issue, User, Event, and API response shapes are either duplicated across the mobile app and backend, or don't exist on the frontend at all. This issue sets up the shared/ directory as a proper TypeScript package that both backend/ and mobile/ (and eventually web/) can import from.

Purpose
A single source of truth for types eliminates a whole class of bugs. When a developer changes Issue.cityRefNumber to Issue.cityReferenceNumber, TypeScript will immediately surface every broken call site across the entire monorepo, instead of discovering it at runtime.

Requirements

  • Initialize shared/ as a TypeScript package (package.json, tsconfig.json)
  • Define and export core domain types: User, Issue, Comment, Upvote, Event, EventRsvp
  • Define and export API request/response types (e.g., CreateIssueRequest, CreateIssueResponse, LoginResponse)
  • Define and export shared enums: IssueCategory, IssueStatus, EventStatus, RsvpStatus
  • Define a generic ApiResponse wrapper type matching backend responses
  • Add shared as a local dependency in backend/package.json and mobile/package.json
  • Confirm TypeScript path resolution works in both packages

Acceptance Criteria

  • shared/ has its own package.json with name @civickit/shared
  • All core domain types are exported from shared/src/index.ts
  • Backend can import import { Issue } from '@civickit/shared' without errors
  • Mobile app can import import { Issue } from '@civickit/shared' without errors
  • Types match the Prisma schema (no conflicting field names)
  • A README.md inside shared/ explains how to add new types

Tech Notes

// backend/package.json and mobile/package.json
{
  "dependencies": {
    "@civickit/shared": "*"
  }
}

Then in the monorepo root run npm install to symlink it.

Keep shared types minimal: only put types here that are truly shared. Backend-only types (Prisma internals, middleware types) stay in backend/. Screen prop types stay in mobile/.

Don't duplicate Prisma: the backend's Prisma client already generates types. The shared types should mirror the API surface (what gets sent over the wire), not the database schema. They'll be similar but not identical (e.g., no passwordHash on the shared User type).

Enums as string unions: prefer type IssueStatus = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' over TypeScript enum for better JSON serialization compatibility with React Native.


Suggested File Structure

civickit/
└── shared/
    ├── package.json
    ├── tsconfig.json
    ├── README.md
    └── src/
        ├── index.ts          ← re-exports everything
        ├── types/
        │   ├── user.ts       ←User, AuthPayload
        │   ├── issue.ts       ←Issue
        │   ├── event.ts     ←Event, EventRsvp
        │   └── api.ts        ← ApiResponse<T>, request/response types
        └── enums/
            ├── issue.ts      ← IssueCategory, IssueStatus
            └── event.ts      ← EventStatus, RsvpStatus

shared/package-json: (should be autogenerated by npm install)

{
  "name": "@civickit/shared",
  "version": "1.0.0",
  "main": "src/index.ts"
}

Example Code

// shared/src/types/issue.ts
export type IssueCategory =
  | 'POTHOLE'
  | 'STREETLIGHT'
  | 'GRAFFITI'
  | 'TRASH'
  | 'OTHER';

export type IssueStatus = 'OPEN' | 'IN_PROGRESS' | 'RESOLVED' | 'CLOSED';

export interface Issue {
  id: string;
  title: string;
  description: string;
  category: IssueCategory;
  status: IssueStatus;
  latitude: number;
  longitude: number;
  address: string;
  images: string[];          // Cloudinary URLs
  cityRefNumber?: string;
  upvoteCount: number;
  createdAt: string;         // ISO string (not Date — safe for JSON)
  author: Pick<User, 'id' | 'name' | 'profileImage'>;
}

// shared/src/types/api.ts
export interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

export interface CreateIssueRequest {
  title: string;
  description: string;
  category: IssueCategory;
  latitude: number;
  longitude: number;
  address: string;
  images?: string[];
}
// Usage in backend controller
import { CreateIssueRequest, ApiResponse, Issue } from '@civickit/shared';

// Usage in mobile screen
import { Issue, IssueStatus } from '@civickit/shared';

Helpful Resources

Coding Journal Prompts

  1. What's the difference between a Prisma-generated type and a shared API type? Why shouldn't they be the same thing?
  2. What fields from the database should never appear in a shared type sent to the client?
  3. Why do we use string for createdAt in the shared type instead of Date?
  4. What's the tradeoff between a string union ('OPEN' | 'CLOSED') and a TypeScript enum?

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions