The Mikado method takes its name from the Mikado game, where the goal is to remove one stick without disturbing the others. The Mikado method has the same philosophy. It aims to make small incremental improvements to a project without breaking the existing codebase.
Have a look at my blog post about the mikado method
Try the Mikado App online here . For now, I am only using the Vercel free plan, meaning the application may be slow.
The functional documentation is available here.
This project is built on top of NEXT.js, which is a framework for React applications. It also uses Supabase as a database, providing an open-source alternative to Firebase.
Before installing the project, you need to install Supabase CLI:
# Install supabase CLI
npx install -g supabase
# Start supabase
npx supabase start
volta is a JavaScript Tool Manager. To install it, please run the following command:
curl https://get.volta.sh | bash
Then, you need to clone and install the project:
git clone [email protected]:arnolanglade/mikado-method.git
# Setup the node version for the project
volta setup
# Install git hooks to prevent pushing code with errors
pnpm setup
# Install the project dependencies
pnpm install
Finally, run the development server:
pnpm dev
Open http://localhost:3000 with your browser to see the result.
# Build the app
pnpm build
# Start the app with the production env
pnpm start
Open http://localhost:3000 with your browser to see the result.
The frontend and the backend are in the same repository. The Next.js convention enforce the following structure:
tree -L 1 .
├── app # contains the application code (frontend and backend)
│ ├── api # only contains the backend API
└── ...
The application code is in the app folder. Next.js uses the folder structure to create the routes. Add a page.tsx file in the app/folder1/folder2 folder, and you will have a route /folder1/folder2. The backend works the same way; add a route.ts file in the app/api/folder1/folder2 folder, and you will have a route /api/folder1/folder2.
There is another folder in the app directory called tools, which is not part of the Next.js convention. It contains tools used by both the frontend (located at the root of the folder) and the backend (in the tools/api folder)."
tree -L 1 .
├── app # contains the application code (frontend and backend)
│ └── tools # contains tools (frontend and backend)
│ ├── ...
│ └── api # only contains the backend tools
The frontend and the backend are in the same repository. The Next.js convention enforces the following structure:
tree -L 1 .
├── supabase
│ ├── ...
│ └── migrations
├── test
└── ...
supabase
folder contains the Supabase migrations and the local configuration, while test
folder contains test utilities like factories, mocks, etc.
The project uses react-intl to handle translations.
First, you need to add a new translation key in translation files (en.ts or fr.ts)
// app/tools/i18n/translation/(en|fr).ts
const translations: Translations = {
myTranslationKey: 'translation...'
};
Then, there is two ways to get a translation, either with a hook or with a component.
From the useIntl
hook:
function MyComponent() {
const {translation} = useIntl()
return <Typography>{translation('myTranslationKey')}</Typography>
}
From the <Translation />
component:
function MyComponent() {
const translation = useIntl()
return <Typography><Translation id="myTranslationKey" /></Typography>
}
For more complex needs, you can pass variables to the translation. Add a placeholder in the translation key between {}
like {name}
.
const translations: Translations = {
myTranslationKey: 'Hello, {name}'
};
Then, you can use the values
property of the Translation
component to replace the placeholder.
<Translation id="myTranslationKey" values={{name: 'Arnaud'}} />
It also works with the useIntl
hook, use the second parameter to pass the values.
const translation = useIntl();
translation('myTranslationKey', {name: 'Arnaud'})
First, you need to update the service container type to add your service:
// app/tools/service-container-context.tsx
export type ServiceContainer = {
myService: (id: string) => string
};
Then, you need to add your service to the service container:
// app/tools/service-container-context.tsx
import myServiceInstance from 'path/service/module'
export const container: ServiceContainer = {
myService: myServiceInstance
};
myService
is the name of your service, use it to get the service from the service container.
Use the useServiceContainer
hook to get the service container:
const { myService } = useServiceContainer();
Run unit tests (frontend and backend)
pnpm unit
Run integration tests (backend only)
pnpm integration
Run linting tool
pnpm lint
Check typescript issues
pnpm tsc
Run all tests
pnpm tests
The createWrapper
function creates a REACT component that initializes all the app's providers. The function must only be used for testing purposes.
render(
<MyComponent>,
{ wrapper: createWrapper(aServiceContainer(), { myTranslationKey: 'translation...' }) },
);
This function takes two arguments: the first allows you to override services, and the second allows you to override translations. The purpose of overriding services is to easily replace them with test doubles, making testing more manageable. Overriding translations aims to enhance test resilience by preventing test failures when translations are modified.
The aServiceContainer
function creates a service container containing all the app's services. It accepts an object as a parameter, where the keys represent the services to be overridden, and the values indicate the new services.
const myService = jest.fn();
render(
<MyComponent>,
{ wrapper: createWrapper(aServiceContainer({myService})) },
);
expect(myService).toBeCalled();
Note: Overriding a service is useful when you need to replace a service with a test double, fake, or mock, for instance. I have written a blog post that explain how the Dependency Inversion design pattern simplify testing.
The createWrapper
function accepts as a second parameter an object of translations that will override the default translations.
render(
<MyComponent>,
{wrapper: createWrapper(aServiceContainer(), {myTranslationKey: 'Validate'})},
);
fireEvent.press(screen.getByText('Validate'));
Note: Overriding a translation makes the test more resilient, even if you change the translation, the test will still pass (be green). Learn how to prevent test suite breaks caused by translation changes in my blog post
In the test-utils.ts
file, you can find some useful functions for simplifying testing, such as factories. They will assist you in creating objects like MikadoGraphView
or MikadoGraph
without the need to specify all the properties. This file also centralizes the object creation process, making it easier to refactor your tests.
aMikadoGraph({
mikadoGraphId: uuidv4(),
goal: 'My goal',
});
Note: I wrote a blog post that explains how to use factories or builders to ease testing.
Start supabase
npx supabase start
Resets the local database to current migrations
npx supabase db reset
Add a new migration
npx supabase migration new <migration name>
npx supabase gen types typescript --local > app/tools/api/supabase/generated-type.ts
npx supabase db reset