Skip to content

Commit b3a691b

Browse files
authored
[microfrontends] Add example for SaaS microservices with UI + API service (#1261)
### Description This PR moves the `saas-microservices` example under the `microfrontends/` folder and changing it to use Express.js. It's done in 2 steps to not break the existing template. ### Demo URL <!-- Provide a URL to a live deployment where we can test your PR. If a demo isn't possible feel free to omit this section. --> ### Type of Change - [ ] New Example - [ ] Example updates (Bug fixes, new features, etc.) - [ ] Other (changes to the codebase, but not to examples) ### New Example Checklist - [ ] 🛫 `npm run new-example` was used to create the example - [ ] 📚 The template wasn't used but I carefuly read the [Adding a new example](https://github.com/vercel/examples#adding-a-new-example) steps and implemented them in the example - [ ] 📱 Is it responsive? Are mobile and tablets considered?
1 parent bf72483 commit b3a691b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+6018
-4
lines changed

microfrontends/nextjs-multi-zones/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ Demo URL: https://vercel-microfrontends-multi-zones.vercel.app/
1414

1515
This example consists of two separate microfrontends that can be deployed independently:
1616

17-
| Application | Description | Deploy |
18-
| ----------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19-
| **Marketing** | Main application handling homepage, pricing, and marketing content | [![Deploy Marketing App](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fmicrofrontends-nextjs-app-multi-zone&project-name=microfrontends-nextjs-app-multi-zone-marketing&repository-name=microfrontends-nextjs-app-multi-zone&root-directory=apps%2Fmarketing) |
20-
| **Documentation** | Dedicated docs microfrontend handling all `/docs` routes | [![Deploy Documentation App](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fmicrofrontends-nextjs-app-multi-zone&project-name=microfrontends-nextjs-app-multi-zone-docs&repository-name=microfrontends-nextjs-app-multi-zone&root-directory=apps%2Fdocs) |
17+
| Application | Description | Deploy |
18+
| ----------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19+
| **Marketing** | Main application handling homepage, pricing, and marketing content | [![Deploy Marketing App](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples&project-name=microfrontends-marketing&repository-name=examples&root-directory=microfrontends/nextjs-multi-zones%2Fapps%2Fmarketing) |
20+
| **Documentation** | Dedicated docs microfrontend handling all `/docs` routes | [![Deploy Documentation App](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples&project-name=microfrontends-docs&repository-name=examples&root-directory=microfrontends/nextjs-multi-zones%2Fapps%docs) |
2121

2222
---
2323

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# Dependencies
4+
node_modules
5+
.pnp
6+
.pnp.js
7+
8+
# Testing
9+
/coverage
10+
11+
# Next.js
12+
.next
13+
out
14+
15+
# Production
16+
build
17+
dist
18+
.output
19+
.nitro
20+
21+
# Misc
22+
.DS_Store
23+
*.pem
24+
tsconfig.tsbuildinfo
25+
26+
# Debug
27+
npm-debug.log*
28+
yarn-debug.log*
29+
yarn-error.log*
30+
31+
# Local ENV files
32+
.env.local
33+
.env.development.local
34+
.env.test.local
35+
.env.production.local
36+
37+
# Vercel
38+
.vercel
39+
40+
# Turborepo
41+
.turbo
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# SaaS Microservices
2+
3+
This is an example SaaS project with 2 API services defined as microservices.
4+
5+
## Getting Started
6+
7+
```sh
8+
pnpm install
9+
pnpm dev
10+
```
11+
12+
Open http://localhost:3024 to view the dashboard. The dashboard will make API calls
13+
to the `api` service for the information to display in the dashboard.
14+
15+
## How It Works
16+
17+
There are 2 separate applications in this example:
18+
19+
- `dashboard` - A [Next.js](https://nextjs.org/) application to show the UI. This application also controls the `microfrontends.json` configuration to route API paths to the other microservices (see below).
20+
- `api` - An [Express.js](https://expressjs.com/) backend serving data displayed in the dashboard.
21+
22+
These all run under the same domain. Paths to each application are routed using Vercel's [microfrontends](https://vercel.com/docs/microfrontends) support:
23+
24+
```mermaid
25+
graph TD
26+
Request --> Vercel{Vercel}
27+
28+
Vercel -->|"/api/*"| Api[api]
29+
Vercel -->|"Everything else"| DashboardApp[Dashboard]
30+
```
31+
32+
## Running Locally
33+
34+
To run all applications together, run:
35+
36+
```sh
37+
pnpm dev
38+
```
39+
40+
A [local development proxy](https://vercel.com/docs/microfrontends/local-development) is automatically run to stitch requests from each application to the local instance of each service.
41+
42+
A single or subset of applications can also be run:
43+
44+
```sh
45+
pnpm dev:dashboard
46+
pnpm dev:api
47+
pnpm turbo run dev -F api -F dashboard
48+
```
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "api",
3+
"version": "1.0.0",
4+
"description": "",
5+
"type": "module",
6+
"keywords": [],
7+
"author": "",
8+
"license": "ISC",
9+
"scripts": {
10+
"dev": "tsx src/index.ts"
11+
},
12+
"dependencies": {
13+
"@types/express": "^5.0.0",
14+
"cookie-parser": "^1.4.7",
15+
"express": "^4.18.2"
16+
},
17+
"devDependencies": {
18+
"@types/node": "^22.0.0",
19+
"tsx": "4.20.6"
20+
}
21+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
import express from "express";
2+
import { fileURLToPath } from "url";
3+
import cookieParser from "cookie-parser";
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
7+
const app = express();
8+
app.use(cookieParser());
9+
10+
interface User {
11+
id: string;
12+
name: string;
13+
}
14+
15+
const USERS: User[] = [
16+
{
17+
id: "1",
18+
name: "John Doe",
19+
},
20+
{
21+
id: "2",
22+
name: "Jane Smith",
23+
},
24+
];
25+
26+
const AUTH_COOKIE_NAME = "saas_microservices_authed_user";
27+
28+
app.get("/api/users/login", (req, res) => {
29+
const existingUser = req.cookies[AUTH_COOKIE_NAME];
30+
if (!existingUser) {
31+
const user = USERS[Math.floor(Math.random() * USERS.length)];
32+
res.cookie(AUTH_COOKIE_NAME, user.id);
33+
}
34+
return res.redirect("/");
35+
});
36+
37+
app.get("/api/users/logout", (req, res) => {
38+
return res.clearCookie(AUTH_COOKIE_NAME).redirect("/");
39+
});
40+
41+
app.get("/api/users/user", (req, res) => {
42+
const existingUser = req.cookies[AUTH_COOKIE_NAME];
43+
if (!existingUser) {
44+
return res.status(404).json({ error: "User not found" });
45+
}
46+
const user = USERS.find((user) => user.id === existingUser);
47+
if (!user) {
48+
return res.status(404).json({ error: "User not found" });
49+
}
50+
return res.json(user);
51+
});
52+
53+
// Fake data generators
54+
const firstNames = [
55+
"Alex",
56+
"Jordan",
57+
"Taylor",
58+
"Morgan",
59+
"Casey",
60+
"Riley",
61+
"Avery",
62+
"Quinn",
63+
"Emma",
64+
"Liam",
65+
"Olivia",
66+
"Noah",
67+
"Ava",
68+
"Ethan",
69+
"Sophia",
70+
"Mason",
71+
"Isabella",
72+
"William",
73+
"Mia",
74+
"James",
75+
"Charlotte",
76+
"Benjamin",
77+
"Amelia",
78+
"Lucas",
79+
"Harper",
80+
"Henry",
81+
"Evelyn",
82+
"Alexander",
83+
"Abigail",
84+
"Michael",
85+
"Emily",
86+
"Daniel",
87+
"Elizabeth",
88+
"Jacob",
89+
"Sofia",
90+
"Logan",
91+
"Avery",
92+
"Jackson",
93+
"Ella",
94+
"Sebastian",
95+
];
96+
97+
const lastNames = [
98+
"Smith",
99+
"Johnson",
100+
"Williams",
101+
"Brown",
102+
"Jones",
103+
"Garcia",
104+
"Miller",
105+
"Davis",
106+
"Rodriguez",
107+
"Martinez",
108+
"Hernandez",
109+
"Lopez",
110+
"Gonzalez",
111+
"Wilson",
112+
"Anderson",
113+
"Thomas",
114+
"Taylor",
115+
"Moore",
116+
"Jackson",
117+
"Martin",
118+
"Lee",
119+
"Perez",
120+
"Thompson",
121+
"White",
122+
"Harris",
123+
"Sanchez",
124+
"Clark",
125+
"Ramirez",
126+
"Lewis",
127+
"Robinson",
128+
"Walker",
129+
"Young",
130+
"Allen",
131+
"King",
132+
"Wright",
133+
"Scott",
134+
"Torres",
135+
"Nguyen",
136+
"Hill",
137+
"Flores",
138+
];
139+
140+
const actions = [
141+
"created a new project",
142+
"deleted a file",
143+
"shared a document",
144+
"commented on a task",
145+
"completed a milestone",
146+
"invited a team member",
147+
"updated project settings",
148+
"exported data report",
149+
"created a new team",
150+
"archived old project",
151+
"updated billing information",
152+
"changed password",
153+
"enabled two-factor authentication",
154+
"uploaded a new file",
155+
"created a backup",
156+
"merged a pull request",
157+
"deployed to production",
158+
"ran automated tests",
159+
"reviewed code changes",
160+
"created a new API key",
161+
"updated integration settings",
162+
"scheduled a meeting",
163+
"published a blog post",
164+
"updated documentation",
165+
"created a new workflow",
166+
"optimized database queries",
167+
"configured monitoring alerts",
168+
"updated security policies",
169+
];
170+
171+
function generateRandomActivity() {
172+
const firstName = firstNames[Math.floor(Math.random() * firstNames.length)];
173+
const lastName = lastNames[Math.floor(Math.random() * lastNames.length)];
174+
const action = actions[Math.floor(Math.random() * actions.length)];
175+
176+
// Generate timestamp within the last 30 days
177+
const now = new Date();
178+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
179+
const randomTime = new Date(
180+
thirtyDaysAgo.getTime() +
181+
Math.random() * (now.getTime() - thirtyDaysAgo.getTime())
182+
);
183+
184+
return {
185+
id: Math.random().toString(36).substr(2, 9),
186+
name: `${firstName} ${lastName}`,
187+
action: action,
188+
timestamp: randomTime.toISOString(),
189+
};
190+
}
191+
192+
app.get("/api/dashboard/activity", (req, res) => {
193+
const activities = Array.from({ length: 20 }, () => generateRandomActivity());
194+
activities.sort(
195+
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
196+
);
197+
198+
res.json({
199+
activities,
200+
total: activities.length,
201+
generated_at: new Date().toISOString(),
202+
});
203+
});
204+
205+
// Health check
206+
app.get("/healthz", (req, res) => {
207+
res.status(200).json({ status: "ok", timestamp: new Date().toISOString() });
208+
});
209+
210+
app.listen(3001, () => {
211+
console.log(`API app listening on port 3001`);
212+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "NodeNext",
5+
"moduleResolution": "NodeNext",
6+
"outDir": "./dist",
7+
"rootDir": ".",
8+
"esModuleInterop": true,
9+
"skipLibCheck": true
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.vercel
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "tailwind.config.ts",
8+
"css": "src/app/globals.css",
9+
"baseColor": "zinc",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"aliases": {
14+
"components": "@/components",
15+
"utils": "@/lib/utils",
16+
"ui": "@/components/ui",
17+
"lib": "@/lib",
18+
"hooks": "@/hooks"
19+
},
20+
"iconLibrary": "lucide"
21+
}

0 commit comments

Comments
 (0)