Skip to content

Instantly share code, notes, and snippets.

@lalainarahajason
Last active August 3, 2025 07:58
Show Gist options
  • Select an option

  • Save lalainarahajason/3cd89a30e751b50c4d236c6569efaee8 to your computer and use it in GitHub Desktop.

Select an option

Save lalainarahajason/3cd89a30e751b50c4d236c6569efaee8 to your computer and use it in GitHub Desktop.
Starter Node.js Clean Architecture + Prisma + Zod

Clean Architecture Starter – Node.js + TypeScript

Minimal starter for scalable, testable Node.js backend projects using Clean Architecture.

🔧 Stack

  • TypeScript + Node.js (Express)
  • Clean Architecture (domain → use cases → infrastructure → interface)
  • Prisma ORM + PostgreSQL
  • Zod for schema validation
  • Vitest for unit testing

📁 Structure simulée (via Gist)

Les noms de fichiers utilisent __ pour simuler l'arborescence. Exemple :
src__application__use-cases__createUser.ts = src/application/use-cases/createUser.ts

🚀 À quoi ça sert ?

Idéal pour :

  • Démarrer une API propre et maintenable
  • Montrer tes compétences d'architecture backend sur GitHub ou Malt
  • Réutiliser pour des projets pro

Made with ❤️ by rahajason

DATABASE_URL=postgresql://user:password@localhost:5432/mydb
//prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid())
name String
email String @unique
createdAt DateTime @default(now())
}
//src/application/use-cases/createUser.ts
import { User } from "@/domain/entities/User";
import { z } from "zod";
export const CreateUserInput = z.object({
name: z.string().min(2),
email: z.string().email()
});
type CreateUserInput = z.infer<typeof CreateUserInput>;
export const createUser = async (
input: CreateUserInput,
deps: { saveUser: (user: Omit<User, "id" | "createdAt">) => Promise<User> }
): Promise<User> => {
const parsed = CreateUserInput.parse(input);
return deps.saveUser(parsed);
};
//src/domain/entities/User.ts
export interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
//src/index.ts
import app from "@/interfaces/http/server";
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
src/infrastructure/prisma/client.ts
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();
// src/infrastructure/repositories/UserRepository.ts
import { User } from "@/domain/entities/User";
import { prisma } from "@/infrastructure/prisma/client";
export const saveUser = async (user: Omit<User, "id" | "createdAt">): Promise<User> => {
const created = await prisma.user.create({
data: user
});
return created;
};
export const getUserById = async (id: string): Promise<User | null> => {
return prisma.user.findUnique({ where: { id } });
};
//src/interfaces/http/routes/users.ts
import express from "express";
import { createUser, CreateUserInput } from "@/application/use-cases/createUser";
import { saveUser } from "@/infrastructure/repositories/UserRepository";
export const userRouter = express.Router();
userRouter.post("/", async (req, res) => {
try {
const user = await createUser(req.body, { saveUser });
res.status(201).json(user);
} catch (err: any) {
res.status(400).json({ error: err.message });
}
});
//src/interfaces/http/server.ts
import express from "express";
import { userRouter } from "@/interfaces/http/routes/users";
const app = express();
app.use(express.json());
app.use("/users", userRouter);
export default app;
//tests/createUser.test.ts
import { describe, it, expect } from "vitest";
import { createUser } from "@/application/use-cases/createUser";
describe("createUser", () => {
it("creates a user with valid data", async () => {
const mockSave = async (data: any) => ({ ...data, id: "1", createdAt: new Date() });
const user = await createUser({ name: "Alice", email: "[email protected]" }, { saveUser: mockSave });
expect(user.name).toBe("Alice");
expect(user.email).toBe("[email protected]");
});
});
{
"compilerOptions": {
"target": "ES2020",
"module": "CommonJS",
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": "./src",
"paths": {
"@/*": ["*"]
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment