Skip to content

Commit

Permalink
Merge pull request #656 from vintasoftware/feat/openapi-ts
Browse files Browse the repository at this point in the history
Set up client API code generation
  • Loading branch information
pamella authored May 17, 2024
2 parents a229adf + 37d8ba0 commit a7033cc
Show file tree
Hide file tree
Showing 22 changed files with 149 additions and 173 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
frontend/bundles/
frontend/webpack_bundles/
frontend/js/api/
12 changes: 11 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -46,4 +45,15 @@ module.exports = {
version: "detect",
},
},
overrides: [
{
files: ["openapi-ts.config.ts"],
rules: {
"import/no-extraneous-dependencies": [
"error",
{ devDependencies: true },
],
},
},
],
};
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
38 changes: 24 additions & 14 deletions .github/workflows/shared-build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions backend/common/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from rest_framework import serializers


class MessageSerializer(serializers.Serializer):
message = serializers.CharField()
40 changes: 20 additions & 20 deletions backend/common/views.py
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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)
17 changes: 11 additions & 6 deletions frontend/js/App.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<Sentry.ErrorBoundary fallback={<p>An error has occurred</p>}>
<Provider store={store}>
<Home />
</Provider>
<Home />
</Sentry.ErrorBoundary>
);

Expand Down
Empty file added frontend/js/api/index.ts
Empty file.
22 changes: 11 additions & 11 deletions frontend/js/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { useState, useEffect } from "react";
import Button from "react-bootstrap/Button";
import { useDispatch, useSelector } from "react-redux";

import DjangoImgSrc from "../../assets/images/django-logo-negative.png";
import { AppDispatch, RootState } from "../store";
import { fetchRestCheck } from "../store/rest_check";
import { RestService } from "../api";

const Home = () => {
const dispatch: AppDispatch = useDispatch();
const restCheck = useSelector((state: RootState) => state.restCheck);
useEffect(() => {
const action = fetchRestCheck();
dispatch(action);
}, [dispatch]);

const [showBugComponent, setShowBugComponent] = useState(false);
const [restCheck, setRestCheck] =
useState<Awaited<ReturnType<typeof RestService.restRestCheckRetrieve>>>();

useEffect(() => {
async function onFetchRestCheck() {
setRestCheck(await RestService.restRestCheckRetrieve());
}
onFetchRestCheck();
}, []);

return (
<>
Expand All @@ -31,7 +31,7 @@ const Home = () => {
<img alt="Django Negative Logo" src={DjangoImgSrc} />
</div>
<h2>Rest API</h2>
<p>{restCheck?.data?.payload?.result}</p>
<p>{restCheck?.message}</p>
<Button variant="outline-dark" onClick={() => setShowBugComponent(true)}>
Click to test if Sentry is capturing frontend errors! (Should only work
in Production)
Expand Down
Loading

0 comments on commit a7033cc

Please sign in to comment.