diff --git a/.eslintignore b/.eslintignore
index 575c8b05..176153ce 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,2 +1,3 @@
frontend/bundles/
frontend/webpack_bundles/
+frontend/js/api/
diff --git a/.eslintrc.js b/.eslintrc.js
index 7b440389..ea0758da 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -5,7 +5,6 @@ module.exports = {
parser: "@typescript-eslint/parser",
extends: ["vinta/recommended-typescript"],
rules: {
- "default-param-last": "off", // due to initialState in Redux
"import/extensions": [
"error",
"ignorePackages",
@@ -46,4 +45,15 @@ module.exports = {
version: "detect",
},
},
+ overrides: [
+ {
+ files: ["openapi-ts.config.ts"],
+ rules: {
+ "import/no-extraneous-dependencies": [
+ "error",
+ { devDependencies: true },
+ ],
+ },
+ },
+ ],
};
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 94c6a2d2..90b839d4 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
ref: main
- run: mkdir -p github/workflows
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 278a8d9f..e51d0e92 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -11,5 +11,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
- uses: ./.github/workflows/shared-build
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 6e2f8084..88a61943 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
- uses: actions/checkout@v2
+ uses: actions/checkout@v4
with:
ref: main
- uses: ./.github/workflows/shared-build
diff --git a/.github/workflows/shared-build/action.yml b/.github/workflows/shared-build/action.yml
index 264e1d9a..d3175aa4 100644
--- a/.github/workflows/shared-build/action.yml
+++ b/.github/workflows/shared-build/action.yml
@@ -11,15 +11,15 @@ runs:
echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Setup Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Setup Node
- uses: actions/setup-node@v2
+ uses: actions/setup-node@v4
with:
- node-version: "18.2"
+ node-version: "18.20"
- name: Cache node modules
- uses: actions/cache@v2
+ uses: actions/cache@v3
env:
cache-name: node-modules-cache
with:
@@ -30,7 +30,7 @@ runs:
build-${{ env.cache-name }}-${{ steps.vars.outputs.branch }}
build-${{ env.cache-name }}
- name: Cache pip
- uses: actions/cache@v2
+ uses: actions/cache@v3
env:
cache-name: pip-cache
with:
@@ -59,15 +59,6 @@ runs:
- run: npm dedupe
working-directory: testproject
shell: bash
- - run: npm run test
- working-directory: testproject
- shell: bash
- - run: npm run lint
- working-directory: testproject
- shell: bash
- - run: npm run build
- working-directory: testproject
- shell: bash
- run: pip install poetry==1.7.1 --upgrade
working-directory: testproject
shell: bash
@@ -90,6 +81,25 @@ runs:
env:
DATABASE_URL: "sqlite:///"
shell: bash
+ - name: Generate backend schema
+ run: poetry run python manage.py spectacular --color --file schema.yml
+ working-directory: testproject/backend
+ env:
+ DATABASE_URL: "sqlite:///"
+ shell: bash
+ - name: Generate frontend API client
+ run: npm run openapi-ts
+ working-directory: testproject
+ shell: bash
+ - run: npm run lint
+ working-directory: testproject
+ shell: bash
+ - run: npm run build
+ working-directory: testproject
+ shell: bash
+ - run: npm run test
+ working-directory: testproject
+ shell: bash
- run: poetry run python manage.py test
working-directory: testproject/backend
env:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 27e7c8b5..3e650eff 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -64,3 +64,9 @@ repos:
language: system
files: ^backend/
pass_filenames: false
+ - id: frontend-api
+ name: frontend-api-local
+ entry: npm run openapi-ts
+ language: system
+ files: backend/schema\.yml$
+ pass_filenames: false
diff --git a/Makefile b/Makefile
index 1cc07e67..2e44504d 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,7 @@ docker_setup:
docker compose build --no-cache backend
docker compose run --rm backend python manage.py spectacular --color --file schema.yml
docker compose run frontend npm install
+ docker compose run --rm frontend npm run openapi-ts
docker_test:
docker compose run backend python manage.py test $(ARG) --parallel --keepdb
@@ -51,3 +52,6 @@ docker_backend_shell:
docker_backend_update_schema:
docker compose run --rm backend python manage.py spectacular --color --file schema.yml
+
+docker_frontend_update_api:
+ docker compose run --rm frontend npm run openapi-ts
diff --git a/README.md b/README.md
index 052752e0..a0830742 100644
--- a/README.md
+++ b/README.md
@@ -40,10 +40,8 @@ Also, includes a Render.com `render.yaml` and a working Django `production.py` s
- State management and backend integration
- `axios` for performing asynchronous calls
- `cookie` for easy integration with Django using the `csrftoken` cookie
- - `@reduxjs/toolkit` for easy state management across the application with the whole toolkit including devtools for inspecting and debugging Redux via browser and ability to run thunks for interacting with the Redux store through asynchronous logic
- - `connected-react-router` for integrating Redux with React Router
+ - `openapi-ts` for generating TypeScript client API code from the backend OpenAPI schema
- `history` for providing browser history to Connected React Router
- - `react-redux` for integrating React with Redux
- Utilities
- `lodash` for general utility functions
- `classnames` for easy working with complex CSS class names on components
@@ -143,13 +141,6 @@ After completing ALL of the above, remove this `Project bootstrap` section from
### If you are not using Docker:
-#### Setup and run the frontend app
-
-- Open a new command line window and go to the project's directory
-- `npm install`
-- `npm run dev`
- - This is used to serve the frontend assets to be consumed by [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) and not to run the React application as usual, so don't worry if you try to check what's running on port 3000 and see an error on your browser
-
#### Setup the backend app
- Open the `backend/.env` file on a text editor and do one of the following:
@@ -169,9 +160,18 @@ After completing ALL of the above, remove this `Project bootstrap` section from
- Run the migrations:
`poetry run python manage.py migrate`
- Generate the OpenAPI schema:
- `poetry run python manage.py spectacular --color --file schema.yml`
+ `poetry run python manage.py spectacular --color --file schema.yml`
- Run the project:
`poetry run python manage.py runserver`
+
+#### Setup and run the frontend app
+
+- Open a new command line window and go to the project's directory
+- `npm install`
+- `npm run openapi-ts`
+ - This is used to generate the TypeScript client API code from the backend OpenAPI schema
+- `npm run dev`
+ - This is used to serve the frontend assets to be consumed by [django-webpack-loader](https://github.com/django-webpack/django-webpack-loader) and not to run the React application as usual, so don't worry if you try to check what's running on port 3000 and see an error on your browser
- Open a browser and go to `http://localhost:8000` to see the project running
#### Setup Celery
@@ -204,19 +204,31 @@ Will run django tests using `--keepdb` and `--parallel`. You may pass a path to
To add a new **backend** dependency, run `poetry add {dependency}`. If the dependency should be only available for development user append `-G dev` to the command.
-### API Schema
+### API Schema and Client generation
-We use the DRF-Spectacular tool to generate an OpenAPI schema from our Django Rest Framework API. The OpenAPI schema serves as the backbone for generating client code, creating comprehensive API documentation, and more.
+We use the [`DRF-Spectacular`](https://drf-spectacular.readthedocs.io/en/latest/readme.html) tool to generate an OpenAPI schema from our Django Rest Framework API. The OpenAPI schema serves as the backbone for generating client code, creating comprehensive API documentation, and more.
The API documentation pages are accessible at `http://localhost:8000/api/schema/swagger-ui/` or `http://localhost:8000/api/schema/redoc/`.
> [!IMPORTANT]
> Anytime a view is created, updated, or removed, the schema must be updated to reflect the changes. Failing to do so can lead to outdated client code or documentation.
->
+>
> To update the schema, run:
> - If you are using Docker: `make docker_backend_update_schema`
> - If you are not using Docker: `poetry run python manage.py spectacular --color --file schema.yml`
+We use the [`openapi-ts`](https://heyapi.vercel.app/openapi-ts/get-started.html) tool to generate TypeScript client code from the OpenAPI schema. The generated client code is used to interact with the API in a type-safe manner.
+
+> [!IMPORTANT]
+> Anytime the API schema is updated, the client code must be regenerated to reflect the changes. Failing to do so can lead to type errors in the client code.
+>
+> To update the client code, run:
+> - If you are using Docker: `make docker_frontend_update_api`
+> - If you are not using Docker: `npm run openapi-ts`
+
+> [!NOTE]
+> If `pre-commit` is properly enabled, it will automatically update both schema and client before each commit whenever necessary.
+
## Github Actions
To enable Continuous Integration through Github Actions, we provide a `proj_main.yml` file. To connect it to Github you need to rename it to `main.yml` and move it to the `.github/workflows/` directory.
diff --git a/backend/common/serializers.py b/backend/common/serializers.py
new file mode 100644
index 00000000..ea75a475
--- /dev/null
+++ b/backend/common/serializers.py
@@ -0,0 +1,5 @@
+from rest_framework import serializers
+
+
+class MessageSerializer(serializers.Serializer):
+ message = serializers.CharField()
diff --git a/backend/common/views.py b/backend/common/views.py
index 6e7b23f8..d8793769 100644
--- a/backend/common/views.py
+++ b/backend/common/views.py
@@ -1,35 +1,34 @@
from django.views import generic
-from drf_spectacular.utils import OpenApiExample, OpenApiResponse, extend_schema
+from drf_spectacular.utils import OpenApiExample, extend_schema
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
+from .serializers import MessageSerializer
+
class IndexView(generic.TemplateView):
template_name = "common/index.html"
class RestViewSet(viewsets.ViewSet):
+ serializer_class = MessageSerializer
+
@extend_schema(
summary="Check REST API",
description="This endpoint checks if the REST API is working.",
- responses={
- 200: OpenApiResponse(
- description="Successful Response",
- examples=[
- OpenApiExample(
- "Example Response",
- value={
- "result": "This message comes from the backend. "
- "If you're seeing this, the REST API is working!"
- },
- response_only=True,
- )
- ],
+ examples=[
+ OpenApiExample(
+ "Successful Response",
+ value={
+ "message": "This message comes from the backend. "
+ "If you're seeing this, the REST API is working!"
+ },
+ response_only=True,
)
- },
+ ],
methods=["GET"],
)
@action(
@@ -39,10 +38,11 @@ class RestViewSet(viewsets.ViewSet):
url_path="rest-check",
)
def rest_check(self, request):
- return Response(
- {
- "result": "This message comes from the backend. "
+ serializer = self.serializer_class(
+ data={
+ "message": "This message comes from the backend. "
"If you're seeing this, the REST API is working!"
- },
- status=status.HTTP_200_OK,
+ }
)
+ serializer.is_valid(raise_exception=True)
+ return Response(serializer.data, status=status.HTTP_200_OK)
diff --git a/frontend/js/App.tsx b/frontend/js/App.tsx
index 76f10896..598eb6b6 100644
--- a/frontend/js/App.tsx
+++ b/frontend/js/App.tsx
@@ -1,15 +1,20 @@
import * as Sentry from "@sentry/react";
-import { Provider } from "react-redux";
+import cookie from "cookie";
+import { OpenAPI } from "./api";
import Home from "./pages/Home";
-import configureStore from "./store";
-const store = configureStore({});
+OpenAPI.interceptors.request.use((request) => {
+ const { csrftoken } = cookie.parse(document.cookie);
+ if (request.headers && csrftoken) {
+ request.headers["X-CSRFTOKEN"] = csrftoken;
+ }
+ return request;
+});
+
const App = () => (
{restCheck?.data?.payload?.result}
+{restCheck?.message}