├── client ├── src │ ├── App.css │ ├── vite-env.d.ts │ ├── index.css │ ├── trpc.ts │ ├── main.tsx │ ├── AppContent.tsx │ ├── components │ │ ├── NotesList.tsx │ │ ├── NoteCard.tsx │ │ └── NoteForm.tsx │ ├── App.tsx │ └── assets │ │ └── react.svg ├── postcss.config.cjs ├── vite.config.ts ├── tsconfig.node.json ├── tailwind.config.cjs ├── .gitignore ├── index.html ├── tsconfig.json ├── package.json ├── public │ └── vite.svg └── pnpm-lock.yaml ├── .gitignore ├── .dockerignore ├── src ├── index.ts ├── config.ts ├── db.ts ├── models │ └── note.ts ├── trpc.ts ├── app.ts └── routes │ └── notes.ts ├── docker-compose.yml ├── Dockerfile ├── README.md ├── package.json ├── tsconfig.json └── pnpm-lock.yaml /client/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | data -------------------------------------------------------------------------------- /client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | background: #202020; 7 | color: white; 8 | } 9 | -------------------------------------------------------------------------------- /client/src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { createTRPCReact } from "@trpc/react-query"; 2 | import type { AppRouter } from "../../src/app"; 3 | 4 | export const trpc = createTRPCReact(); 5 | -------------------------------------------------------------------------------- /client/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import app from "./app"; 2 | import { dbConnect } from "./db"; 3 | import { PORT } from "./config"; 4 | 5 | dbConnect(); 6 | app.listen(PORT as number); 7 | console.log("Server is running on port 3000"); 8 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | 3 | config(); 4 | 5 | export const PORT = parseInt(process.env.PORT || "") || 3000; 6 | export const MONGODB_URI = 7 | process.env.MONGODB_URI || "mongodb://localhost:27017"; 8 | -------------------------------------------------------------------------------- /client/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /client/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | } 12 | -------------------------------------------------------------------------------- /client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /client/src/AppContent.tsx: -------------------------------------------------------------------------------- 1 | import { NoteForm } from "./components/NoteForm"; 2 | import { NotesList } from "./components/NotesList"; 3 | 4 | export function AppContent() { 5 | return ( 6 |
7 |

Notes

8 | 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/db.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { MONGODB_URI } from "./config"; 3 | 4 | export const dbConnect = async () => { 5 | try { 6 | mongoose.set("strictQuery", false); 7 | const db = await mongoose.connect(MONGODB_URI); 8 | console.log("Database connected to ", db.connection.db.databaseName); 9 | } catch (error) { 10 | if (error instanceof Error) { 11 | console.error(error.message); 12 | process.exit(1) 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/models/note.ts: -------------------------------------------------------------------------------- 1 | import { getModelForClass, modelOptions, prop } from "@typegoose/typegoose"; 2 | 3 | @modelOptions({ 4 | schemaOptions: { 5 | timestamps: true, 6 | }, 7 | }) 8 | class Note { 9 | @prop({ 10 | type: String, 11 | }) 12 | title: string; 13 | 14 | @prop({ 15 | type: String, 16 | }) 17 | description: string; 18 | 19 | @prop({ type: Boolean, default: false }) 20 | done: boolean; 21 | } 22 | 23 | export default getModelForClass(Note); 24 | -------------------------------------------------------------------------------- /client/src/components/NotesList.tsx: -------------------------------------------------------------------------------- 1 | import { NoteCard } from "./NoteCard"; 2 | import { trpc } from "../trpc"; 3 | 4 | export function NotesList() { 5 | const { data, isError, isLoading, error } = trpc.note.get.useQuery(); 6 | 7 | if (isLoading) return
Loading...
; 8 | if (isError) return
{error.message}
; 9 | 10 | return ( 11 |
12 | {(data || []).map((note: any) => ( 13 | 14 | ))} 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | web: 5 | container_name: web 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | environment: 10 | - PORT=3000 11 | - MONGODB_URI=mongodb://db/mern-trpc 12 | ports: 13 | - 3000:3000 14 | depends_on: 15 | - db 16 | db: 17 | container_name: db 18 | image: mongo 19 | volumes: 20 | - ./data/db:/data/db 21 | ports: 22 | - 27017:27017 23 | logging: 24 | driver: none -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | COPY tsconfig.json ./ 7 | 8 | # copy the backend 9 | COPY src ./src 10 | 11 | # install backend dependencies 12 | RUN npm install 13 | 14 | EXPOSE 3000 15 | 16 | # RUN npm run build:back 17 | 18 | # copy the frontend 19 | 20 | COPY client ./client 21 | 22 | # install frontend dependencies 23 | RUN npm install --prefix client 24 | 25 | # RUN npm run build:front 26 | 27 | RUN npm run build:all 28 | 29 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { inferAsyncReturnType, initTRPC } from "@trpc/server"; 2 | import * as trpcExpress from "@trpc/server/adapters/express"; 3 | 4 | // created for each request 5 | export const createContext = ({ 6 | req, 7 | res, 8 | }: trpcExpress.CreateExpressContextOptions) => ({}); // no context 9 | type Context = inferAsyncReturnType; 10 | 11 | const t = initTRPC.context().create(); 12 | 13 | export const router = t.router; 14 | export const middleware = t.middleware; 15 | export const publicProcedure = t.procedure; -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { notesRouter } from "./routes/notes"; 3 | import * as trpcExpress from "@trpc/server/adapters/express"; 4 | import { router, createContext } from "./trpc"; 5 | import cors from "cors"; 6 | import path from "path"; 7 | 8 | const app = express(); 9 | 10 | const appRouter = router({ 11 | note: notesRouter, 12 | }); 13 | 14 | app.use(cors()); 15 | 16 | app.use( 17 | "/trpc", 18 | trpcExpress.createExpressMiddleware({ 19 | router: appRouter, 20 | createContext, 21 | }) 22 | ); 23 | 24 | app.use(express.static(path.join(__dirname, "../client/dist"))); 25 | 26 | export type AppRouter = typeof appRouter; 27 | 28 | export default app; 29 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "strictPropertyInitialization": false 21 | }, 22 | "include": ["src"], 23 | "references": [{ "path": "./tsconfig.node.json" }] 24 | } 25 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "@tanstack/react-query": "^4.24.4", 13 | "@trpc/client": "^10.10.0", 14 | "@trpc/react-query": "^10.10.0", 15 | "@trpc/server": "^10.10.0", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^18.0.27", 21 | "@types/react-dom": "^18.0.10", 22 | "@vitejs/plugin-react": "^3.1.0", 23 | "autoprefixer": "^10.4.13", 24 | "postcss": "^8.4.21", 25 | "tailwindcss": "^3.2.4", 26 | "typescript": "^4.9.5", 27 | "vite": "^4.1.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 3 | import { trpc } from "./trpc"; 4 | import { httpBatchLink } from "@trpc/client"; 5 | import { AppContent } from "./AppContent"; 6 | 7 | function App() { 8 | const [queryClient] = useState(() => new QueryClient()); 9 | const [trpcClient] = useState(() => 10 | trpc.createClient({ 11 | links: [ 12 | httpBatchLink({ 13 | url: "http://localhost:3000/trpc", 14 | }), 15 | ], 16 | }) 17 | ); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### MERN tRPC CRUD 2 | 3 | A MERN(Mongodb, Express, React, Node) CRUD application using tRPC as library to communicate frontend and backend using Typescript. 4 | 5 | ### Installation 6 | 7 | * Installation requires a mongodb database connection 8 | 9 | ``` 10 | git clone git@github.com:faztweb/mern-trpc-crud.git 11 | cd mern-trpc-crud 12 | npm i 13 | npm run build # build the frontend and backend 14 | npm start 15 | ``` 16 | Now you can visit [http://localhost:3000](http://localhost:3000) 17 | 18 | ### Docker (Recommended) 19 | 20 | ``` 21 | git clone git@github.com:faztweb/mern-trpc-crud.git 22 | cd mern-trpc-crud 23 | docker-compose up 24 | ``` 25 | 26 | Now you can visit [http://localhost:3000](http://localhost:3000) 27 | 28 | ### Deployment 29 | 30 | ``` 31 | git clone git@github.com:faztweb/mern-trpc-crud.git 32 | cd mern-trpc-crud 33 | npm i 34 | npm run build:prod 35 | ``` 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern-trpc-crud", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "ts-node-dev src/index.ts", 8 | "build:back": "tsc", 9 | "start": "node dist/index.js", 10 | "build:front": "npm run build --prefix client", 11 | "build:prod": "npm run build:back && npm run build:front", 12 | "clean": "rm -rf dist && rm -rf client/dist && rm -rf data" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@trpc/server": "^10.10.0", 19 | "@typegoose/typegoose": "^10.1.0", 20 | "cors": "^2.8.5", 21 | "dotenv": "^16.0.3", 22 | "express": "^4.18.2", 23 | "mongoose": "^6.9.0", 24 | "zod": "^3.20.2" 25 | }, 26 | "devDependencies": { 27 | "@types/cors": "^2.8.13", 28 | "@types/express": "^4.17.17", 29 | "@types/node": "^18.11.18", 30 | "ts-node-dev": "^2.0.0", 31 | "typescript": "^4.9.5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/notes.ts: -------------------------------------------------------------------------------- 1 | import { publicProcedure, router } from "../trpc"; 2 | import Note from "../models/note"; 3 | import { z } from "zod"; 4 | 5 | const getNotes = publicProcedure.query(async () => { 6 | const notes = await Note.find(); 7 | return notes; 8 | }); 9 | 10 | const createNotes = publicProcedure 11 | .input( 12 | z.object({ 13 | title: z.string(), 14 | description: z.string(), 15 | }) 16 | ) 17 | .mutation(async ({ input: { title, description } }) => { 18 | const newNote = new Note({ title, description }); 19 | const savedNote = await newNote.save(); 20 | return savedNote; 21 | }); 22 | 23 | const deleteNote = publicProcedure 24 | .input(z.string()) 25 | .mutation(async ({ input }) => { 26 | const deletedTask = await Note.findByIdAndDelete(input); 27 | if (!deletedTask) throw new Error("Note not found"); 28 | return true; 29 | }); 30 | 31 | const toggleDone = publicProcedure 32 | .input(z.string()) 33 | .mutation(async ({ input }) => { 34 | try { 35 | const foundNote = await Note.findById(input); 36 | if (!foundNote) throw new Error("Note not found"); 37 | foundNote.done = !foundNote.done; 38 | await foundNote.save(); 39 | return true; 40 | } catch (error) { 41 | console.error(error); 42 | return false; 43 | } 44 | }); 45 | 46 | export const notesRouter = router({ 47 | create: createNotes, 48 | delete: deleteNote, 49 | get: getNotes, 50 | toggleDone 51 | }); 52 | -------------------------------------------------------------------------------- /client/src/components/NoteCard.tsx: -------------------------------------------------------------------------------- 1 | import { trpc } from "../trpc"; 2 | 3 | export function NoteCard({ note }: any) { 4 | const deleteNote = trpc.note.delete.useMutation(); 5 | const toggleNoteDone = trpc.note.toggleDone.useMutation(); 6 | const context = trpc.useContext(); 7 | 8 | const onDeleteNote = () => { 9 | deleteNote.mutate(note._id, { 10 | onSuccess(data) { 11 | if (data) { 12 | context.note.get.invalidate(); 13 | } 14 | }, 15 | onError(error) { 16 | alert(error.message); 17 | }, 18 | }); 19 | }; 20 | 21 | const onToggleDone = () => { 22 | toggleNoteDone.mutate(note._id, { 23 | onSuccess(data) { 24 | if (data) { 25 | context.note.get.invalidate(); 26 | } 27 | }, 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 |
34 |

{note.title}

35 |

{note.description}

36 |
37 | 43 | 44 | 52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /client/src/components/NoteForm.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, FormEvent, useRef, useState } from "react"; 2 | import { trpc } from "../trpc"; 3 | 4 | const initialState = { 5 | title: "", 6 | description: "", 7 | }; 8 | 9 | export function NoteForm() { 10 | const [note, setNote] = useState(initialState); 11 | const addNote = trpc.note.create.useMutation(); 12 | const utils = trpc.useContext(); 13 | const titleRef = useRef(null); 14 | 15 | const handleChange = ( 16 | e: ChangeEvent 17 | ) => setNote({ ...note, [e.target.name]: e.target.value }); 18 | 19 | const handleSubmit = (e: FormEvent) => { 20 | e.preventDefault(); 21 | addNote.mutate(note, { 22 | onSuccess: () => { 23 | // utils.invalidateQueries(["getNotes"]); 24 | utils.note.get.invalidate() 25 | setNote(initialState); 26 | titleRef.current?.focus(); 27 | }, 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 | 43 |