98106de
commit 98106de
37 files changed
+2546 -33
Showing 37 changed files+2546-33
docs_site/astro.config.mjsdocs_site/src/content/docs/how-to/graphql.mddocs_site/src/content/docs/how-to/index.mddocs_site/src/content/docs/resources/about.mddocs_site/src/content/docs/resources/external-links.mddocs_site/src/content/docs/resources/help.mddocs_site/src/content/docs/tutorials/async-await.mddocs_site/src/content/docs/tutorials/body-updates.mddocs_site/src/content/docs/tutorials/cookie-params.mddocs_site/src/content/docs/tutorials/cors.mddocs_site/src/content/docs/tutorials/debugging.mddocs_site/src/content/docs/tutorials/dependencies/classes-as-dependencies/index.mddocs_site/src/content/docs/tutorials/dependencies/dependencies-in-path-ops/index.mddocs_site/src/content/docs/tutorials/dependencies/dependencies-with-yield/index.mddocs_site/src/content/docs/tutorials/dependencies/global-dependencies/index.mddocs_site/src/content/docs/tutorials/dependencies/sub-dependencies/index.mddocs_site/src/content/docs/tutorials/encoder.mddocs_site/src/content/docs/tutorials/extra-data-types.mddocs_site/src/content/docs/tutorials/extra-models.mddocs_site/src/content/docs/tutorials/form-data.mddocs_site/src/content/docs/tutorials/form-models.mddocs_site/src/content/docs/tutorials/header-params.mddocs_site/src/content/docs/tutorials/metadata.mddocs_site/src/content/docs/tutorials/path-operation-config.mddocs_site/src/content/docs/tutorials/path-params.mddocs_site/src/content/docs/tutorials/python-types.mddocs_site/src/content/docs/tutorials/query-params.mddocs_site/src/content/docs/tutorials/request-files.mddocs_site/src/content/docs/tutorials/request-forms-files.mddocs_site/src/content/docs/tutorials/response-model.mddocs_site/src/content/docs/tutorials/response-status-code.mddocs_site/src/content/docs/tutorials/security/first-steps/index.mddocs_site/src/content/docs/tutorials/security/get-current-user/index.mddocs_site/src/content/docs/tutorials/security/oauth2-jwt/index.mddocs_site/src/content/docs/tutorials/security/simple-oauth2/index.mddocs_site/src/content/docs/tutorials/static-files.mddocs_site/src/content/docs/tutorials/testing.md
▼docs_site/astro.config.mjs
+116-33
| @@ -108,19 +108,108 @@ export default defineConfig({ | ||
108 | 108 | ], |
109 | 109 | }, |
110 | 110 | { |
111 | - label: 'Tutorials', | |
111 | + label: 'Tutorial — User Guide', | |
112 | + badge: { text: 'Start here', variant: 'success' }, | |
112 | 113 | items: [ |
113 | - { label: 'Hello World', link: 'tutorials/hello-world/' }, | |
114 | - { label: 'Path & Query Parameters', link: 'tutorials/path-query-params/' }, | |
115 | - { label: 'Request Body & Validation', link: 'tutorials/request-body/' }, | |
116 | - { label: 'Dependency Injection', link: 'tutorials/dependency-injection/' }, | |
114 | + { label: 'Python Types Intro', link: 'tutorials/python-types/' }, | |
115 | + { label: 'Async / Await', link: 'tutorials/async-await/' }, | |
116 | + { label: 'First Steps', link: 'tutorials/hello-world/' }, | |
117 | + { label: 'Path Parameters', link: 'tutorials/path-params/' }, | |
118 | + { label: 'Query Parameters', link: 'tutorials/query-params/' }, | |
119 | + { label: 'Request Body (Pydantic)', link: 'tutorials/request-body/' }, | |
120 | + { label: 'Query Parameters & String Validations', link: 'tutorials/query-params-str-validations/' }, | |
121 | + { label: 'Path Parameters & Numeric Validations', link: 'tutorials/path-params-numeric-validations/' }, | |
122 | + { label: 'Body — Multiple Parameters', link: 'tutorials/body-multiple-params/' }, | |
123 | + { label: 'Body — Fields', link: 'tutorials/body-fields/' }, | |
124 | + { label: 'Body — Nested Models', link: 'tutorials/body-nested-models/' }, | |
125 | + { label: 'Extra Data Types', link: 'tutorials/extra-data-types/' }, | |
126 | + { label: 'Cookie Parameters', link: 'tutorials/cookie-params/' }, | |
127 | + { label: 'Header Parameters', link: 'tutorials/header-params/' }, | |
128 | + { label: 'Response Model — Return Type', link: 'tutorials/response-model/' }, | |
129 | + { label: 'Extra Models', link: 'tutorials/extra-models/' }, | |
130 | + { label: 'Response Status Code', link: 'tutorials/response-status-code/' }, | |
131 | + { label: 'Form Data', link: 'tutorials/form-data/' }, | |
132 | + { label: 'Form Models', link: 'tutorials/form-models/' }, | |
133 | + { label: 'Request Files', link: 'tutorials/request-files/' }, | |
134 | + { label: 'Request Forms & Files', link: 'tutorials/request-forms-files/' }, | |
135 | + { label: 'Handling Errors', link: 'tutorials/error-handling/' }, | |
136 | + { label: 'Path Operation Configuration', link: 'tutorials/path-operation-config/' }, | |
137 | + { label: 'JSON Compatible Encoder', link: 'tutorials/encoder/' }, | |
138 | + { label: 'Body — Updates', link: 'tutorials/body-updates/' }, | |
139 | + { | |
140 | + label: 'Dependencies', | |
141 | + collapsed: true, | |
142 | + items: [ | |
143 | + { label: 'Classes as Dependencies', link: 'tutorials/dependencies/classes-as-dependencies/' }, | |
144 | + { label: 'Sub-dependencies', link: 'tutorials/dependencies/sub-dependencies/' }, | |
145 | + { label: 'Dependencies in Path Ops', link: 'tutorials/dependencies/dependencies-in-path-ops/' }, | |
146 | + { label: 'Global Dependencies', link: 'tutorials/dependencies/global-dependencies/' }, | |
147 | + { label: 'Dependencies with yield', link: 'tutorials/dependencies/dependencies-with-yield/' }, | |
148 | + ], | |
149 | + }, | |
150 | + { | |
151 | + label: 'Security', | |
152 | + collapsed: true, | |
153 | + items: [ | |
154 | + { label: 'Security — First Steps', link: 'tutorials/security/first-steps/' }, | |
155 | + { label: 'Get Current User', link: 'tutorials/security/get-current-user/' }, | |
156 | + { label: 'Simple OAuth2', link: 'tutorials/security/simple-oauth2/' }, | |
157 | + { label: 'OAuth2 + JWT Tokens', link: 'tutorials/security/oauth2-jwt/' }, | |
158 | + ], | |
159 | + }, | |
117 | 160 | { label: 'Middleware', link: 'tutorials/middleware/' }, |
118 | - { label: 'Error Handling', link: 'tutorials/error-handling/' }, | |
119 | - { label: 'File Uploads', link: 'tutorials/file-uploads/' }, | |
120 | - { label: 'WebSockets & SSE', link: 'tutorials/websockets-sse/' }, | |
121 | - { label: 'Database Integration', link: 'tutorials/database-integration/' }, | |
161 | + { label: 'CORS (Cross-Origin Resource Sharing)', link: 'tutorials/cors/' }, | |
162 | + { label: 'SQL Databases', link: 'tutorials/database-integration/' }, | |
163 | + { label: 'Bigger Applications — Multiple Files', link: 'tutorials/routing-subrouters/' }, | |
122 | 164 | { label: 'Background Tasks', link: 'tutorials/background-tasks/' }, |
123 | - { label: 'Routing & Sub-routers', link: 'tutorials/routing-subrouters/' }, | |
165 | + { label: 'Metadata & Docs URLs', link: 'tutorials/metadata/' }, | |
166 | + { label: 'Static Files', link: 'tutorials/static-files/' }, | |
167 | + { label: 'Testing', link: 'tutorials/testing/' }, | |
168 | + { label: 'Debugging', link: 'tutorials/debugging/' }, | |
169 | + ], | |
170 | + }, | |
171 | + { | |
172 | + label: 'Advanced User Guide', | |
173 | + items: [ | |
174 | + { label: 'Zero-GIL Architecture', link: 'advanced/zero-gil-architecture/' }, | |
175 | + { label: 'Rust Core Deep Dive', link: 'advanced/rust-core-deep-dive/' }, | |
176 | + { label: 'PyO3 & FFI Safety', link: 'advanced/pyo3-ffi-safety/' }, | |
177 | + { label: 'Native Fast Path', link: 'advanced/native-fast-path/' }, | |
178 | + { label: 'Multi-Protocol APIs', link: 'advanced/multi-protocol-apis/' }, | |
179 | + { label: 'Agent System', link: 'advanced/agent-system/' }, | |
180 | + { label: 'Streaming Output', link: 'advanced/streaming-output/' }, | |
181 | + { label: 'Performance Tuning', link: 'advanced/performance-tuning/' }, | |
182 | + { label: 'Resilience Patterns', link: 'advanced/resilience-patterns/' }, | |
183 | + { label: 'Return a Response Directly', link: 'advanced/response-directly/' }, | |
184 | + { label: 'Custom Response Classes', link: 'advanced/custom-response/' }, | |
185 | + { label: 'Additional Responses in OpenAPI', link: 'advanced/additional-responses/' }, | |
186 | + { label: 'Response Cookies & Headers', link: 'advanced/response-cookies-headers/' }, | |
187 | + { label: 'Advanced Dependencies', link: 'advanced/advanced-dependencies/' }, | |
188 | + { label: 'Advanced Security', link: 'advanced/advanced-security/' }, | |
189 | + { label: 'Using the Request Directly', link: 'advanced/using-request-directly/' }, | |
190 | + { label: 'Using Dataclasses', link: 'advanced/dataclasses/' }, | |
191 | + { label: 'Advanced Middleware', link: 'advanced/advanced-middleware/' }, | |
192 | + { label: 'Sub Applications — Mounts', link: 'advanced/sub-applications/' }, | |
193 | + { label: 'Behind a Proxy', link: 'advanced/behind-a-proxy/' }, | |
194 | + { label: 'Templates (Jinja2)', link: 'advanced/templates/' }, | |
195 | + { label: 'WebSockets', link: 'advanced/websockets-advanced/' }, | |
196 | + { label: 'Lifespan Events', link: 'advanced/lifespan-events/' }, | |
197 | + { label: 'Async Tests', link: 'advanced/async-tests/' }, | |
198 | + { label: 'Settings & Environment Variables', link: 'advanced/settings/' }, | |
199 | + { label: 'OpenAPI Callbacks & Webhooks', link: 'advanced/openapi-callbacks/' }, | |
200 | + { label: 'Generating SDKs', link: 'advanced/generate-clients/' }, | |
201 | + ], | |
202 | + }, | |
203 | + { | |
204 | + label: 'How-To Recipes', | |
205 | + items: [ | |
206 | + { label: 'Overview', link: 'how-to/' }, | |
207 | + { label: 'GraphQL Integration', link: 'how-to/graphql/' }, | |
208 | + { label: 'Custom Request & Route Classes', link: 'how-to/custom-request-route/' }, | |
209 | + { label: 'Configure Swagger UI', link: 'how-to/configure-swagger-ui/' }, | |
210 | + { label: 'Testing a Database', link: 'how-to/testing-database/' }, | |
211 | + { label: 'Circuit Breaker Recipes', link: 'how-to/circuit-breaker-recipes/' }, | |
212 | + { label: 'Background Task Patterns', link: 'how-to/background-task-patterns/' }, | |
124 | 213 | ], |
125 | 214 | }, |
126 | 215 | { |
| @@ -145,29 +234,6 @@ export default defineConfig({ | ||
145 | 234 | { label: 'Database', link: 'api-reference/database/' }, |
146 | 235 | ], |
147 | 236 | }, |
148 | - { | |
149 | - label: 'Advanced Guides', | |
150 | - items: [ | |
151 | - { label: 'Zero-GIL Architecture', link: 'advanced/zero-gil-architecture/' }, | |
152 | - { label: 'Rust Core Deep Dive', link: 'advanced/rust-core-deep-dive/' }, | |
153 | - { label: 'PyO3 & FFI Safety', link: 'advanced/pyo3-ffi-safety/' }, | |
154 | - { label: 'Native Fast Path', link: 'advanced/native-fast-path/' }, | |
155 | - { label: 'Multi-Protocol APIs', link: 'advanced/multi-protocol-apis/' }, | |
156 | - { label: 'Agent System', link: 'advanced/agent-system/' }, | |
157 | - { label: 'Streaming Output', link: 'advanced/streaming-output/' }, | |
158 | - { label: 'Performance Tuning', link: 'advanced/performance-tuning/' }, | |
159 | - { label: 'Resilience Patterns', link: 'advanced/resilience-patterns/' }, | |
160 | - ], | |
161 | - }, | |
162 | - { | |
163 | - label: 'Inference', | |
164 | - items: [ | |
165 | - { label: 'Overview', link: 'inference/overview/' }, | |
166 | - { label: 'LLM Serving API', link: 'inference/llm-serving-api/' }, | |
167 | - { label: 'GPU & CUDA Setup', link: 'inference/gpu-cuda-setup/' }, | |
168 | - { label: 'Scheduling & Batching', link: 'inference/scheduling-batching/' }, | |
169 | - ], | |
170 | - }, | |
171 | 237 | { |
172 | 238 | label: 'Deployment', |
173 | 239 | items: [ |
| @@ -200,6 +266,15 @@ export default defineConfig({ | ||
200 | 266 | { label: 'Health Checks', link: 'observability/health-checks/' }, |
201 | 267 | ], |
202 | 268 | }, |
269 | + { | |
270 | + label: 'Inference', | |
271 | + items: [ | |
272 | + { label: 'Overview', link: 'inference/overview/' }, | |
273 | + { label: 'LLM Serving API', link: 'inference/llm-serving-api/' }, | |
274 | + { label: 'GPU & CUDA Setup', link: 'inference/gpu-cuda-setup/' }, | |
275 | + { label: 'Scheduling & Batching', link: 'inference/scheduling-batching/' }, | |
276 | + ], | |
277 | + }, | |
203 | 278 | { |
204 | 279 | label: 'Reference', |
205 | 280 | items: [ |
| @@ -211,6 +286,14 @@ export default defineConfig({ | ||
211 | 286 | { label: 'Glossary', link: 'reference/glossary/' }, |
212 | 287 | ], |
213 | 288 | }, |
289 | + { | |
290 | + label: 'Resources', | |
291 | + items: [ | |
292 | + { label: 'Help & Support', link: 'resources/help/' }, | |
293 | + { label: 'External Links & Ecosystem', link: 'resources/external-links/' }, | |
294 | + { label: 'About JustAPI', link: 'resources/about/' }, | |
295 | + ], | |
296 | + }, | |
214 | 297 | { |
215 | 298 | label: 'Contributing', |
216 | 299 | items: [ |
217 | 300 | |
▼docs_site/src/content/docs/how-to/graphql.md
+95-0
| @@ -0,0 +1,95 @@ | ||
1 | +--- | |
2 | +title: GraphQL Integration | |
3 | +description: Set up Strawberry GraphQL alongside REST endpoints in a JustAPI application. | |
4 | +keywords: [JustAPI, GraphQL, Strawberry, REST, multi-protocol] | |
5 | +--- | |
6 | + | |
7 | +JustAPI supports running GraphQL alongside REST endpoints in the same application. | |
8 | + | |
9 | +## Basic Setup | |
10 | + | |
11 | +Install Strawberry: | |
12 | + | |
13 | +```bash | |
14 | +pip install strawberry-graphql | |
15 | +``` | |
16 | + | |
17 | +Create your GraphQL schema: | |
18 | + | |
19 | +```python | |
20 | +import strawberry | |
21 | +from justapi import JustAPIApp | |
22 | + | |
23 | +@strawberry.type | |
24 | +class User: | |
25 | + id: int | |
26 | + name: str | |
27 | + email: str | |
28 | + | |
29 | +@strawberry.type | |
30 | +class Query: | |
31 | + @strawberry.field | |
32 | + def users(self) -> list[User]: | |
33 | + return [User(id=1, name="Alice", email="[email protected]")] | |
34 | + | |
35 | +@strawberry.type | |
36 | +class Mutation: | |
37 | + @strawberry.mutation | |
38 | + def create_user(self, name: str, email: str) -> User: | |
39 | + return User(id=2, name=name, email=email) | |
40 | +``` | |
41 | + | |
42 | +Mount the GraphQL endpoint in your JustAPI app: | |
43 | + | |
44 | +```python | |
45 | +import strawberry | |
46 | +from strawberry.fastapi import GraphQLRouter | |
47 | + | |
48 | +app = JustAPIApp() | |
49 | + | |
50 | +schema = strawberry.Schema(query=Query, mutation=Mutation) | |
51 | +graphql_app = GraphQLRouter(schema) | |
52 | + | |
53 | +app.include_router(graphql_app, prefix="/graphql") | |
54 | +``` | |
55 | + | |
56 | +## Testing GraphQL | |
57 | + | |
58 | +Start your app and visit `http://localhost:8000/graphql` for the GraphiQL interactive explorer. | |
59 | + | |
60 | +```graphql | |
61 | +query { | |
62 | + users { | |
63 | + id | |
64 | + name | |
65 | + email | |
66 | + } | |
67 | +} | |
68 | +``` | |
69 | + | |
70 | +## Shared Authentication | |
71 | + | |
72 | +JustAPI supports global dependencies that apply to all routes including GraphQL: | |
73 | + | |
74 | +```python | |
75 | +from justapi import Depends, Security | |
76 | + | |
77 | +def get_current_user(request): | |
78 | + token = request.headers.get("Authorization", "").replace("Bearer ", "") | |
79 | + # validate token... | |
80 | + return {"user_id": 1, "name": "Alice"} | |
81 | + | |
82 | +app = JustAPIApp(dependencies=[Depends(get_current_user)]) | |
83 | +``` | |
84 | + | |
85 | +## Recap | |
86 | + | |
87 | +- Install `strawberry-graphql` for GraphQL support. | |
88 | +- Mount `GraphQLRouter` with a prefix using `app.include_router()`. | |
89 | +- GraphQL runs alongside REST — no separate server needed. | |
90 | +- Use JustAPI's dependency injection for shared auth. | |
91 | + | |
92 | +## See Also | |
93 | + | |
94 | +- [Routing & Sub-routers](/tutorials/routing-subrouters/) — multiple routers | |
95 | +- [Security](/tutorials/security/first-steps/) — authentication | |
0 | 96 | |
▼docs_site/src/content/docs/how-to/index.md
+24-0
| @@ -0,0 +1,24 @@ | ||
1 | +--- | |
2 | +title: How-To Recipes | |
3 | +description: Standalone recipes for common tasks in JustAPI — GraphQL, custom middleware, database testing, and more. | |
4 | +keywords: [JustAPI, how-to, recipes, tips, patterns] | |
5 | +--- | |
6 | + | |
7 | +These are standalone, task-oriented guides for specific JustAPI use cases. Each recipe solves a concrete problem without requiring you to read a full tutorial. | |
8 | + | |
9 | +## Available Recipes | |
10 | + | |
11 | +| Recipe | Problem Solved | | |
12 | +|--------|----------------| | |
13 | +| [GraphQL Integration](/how-to/graphql/) | Set up Strawberry GraphQL alongside REST endpoints | | |
14 | +| [Custom Request & Route Classes](/how-to/custom-request-route/) | Extend `Request` and `APIRoute` for custom behavior | | |
15 | +| [Configure Swagger UI](/how-to/configure-swagger-ui/) | Customize the interactive docs appearance | | |
16 | +| [Testing a Database](/how-to/testing-database/) | Use in-memory SQLite for fast test isolation | | |
17 | +| [Circuit Breaker Recipes](/how-to/circuit-breaker-recipes/) | Configure resilience patterns for real-world scenarios | | |
18 | +| [Background Task Patterns](/how-to/background-task-patterns/) | Prioritized tasks, progress tracking, error handling | | |
19 | + | |
20 | +## See Also | |
21 | + | |
22 | +- [Tutorial — User Guide](/tutorials/hello-world/) — learn JustAPI from scratch | |
23 | +- [Advanced User Guide](/advanced/zero-gil-architecture/) — deep-dive topics | |
24 | +- [API Reference](/api-reference/) — complete class and function reference | |
0 | 25 | |
▼docs_site/src/content/docs/resources/about.md
+66-0
| @@ -0,0 +1,66 @@ | ||
1 | +--- | |
2 | +title: About JustAPI | |
3 | +description: The story, philosophy, and future of JustAPI — a Rust-powered Python web framework built to replace FastAPI. | |
4 | +keywords: [JustAPI, about, history, philosophy, Rust, Python web framework] | |
5 | +--- | |
6 | + | |
7 | +## What is JustAPI? | |
8 | + | |
9 | +JustAPI is a Python web framework for building APIs, powered by a Rust core. It provides the same developer experience as FastAPI — decorator syntax, Pydantic models, dependency injection — while moving the entire networking stack out of Python. | |
10 | + | |
11 | +**Python writes the logic. Rust does everything else.** | |
12 | + | |
13 | +## Why Rust? | |
14 | + | |
15 | +Python is excellent for application logic. It's readable, has a rich ecosystem, and developers love it. But it has limitations for networking: | |
16 | + | |
17 | +- The GIL prevents true parallelism in Python handlers. | |
18 | +- Python HTTP servers (uvicorn, gunicorn) add overhead. | |
19 | +- TLS termination, connection management, and routing are all CPU-bound. | |
20 | + | |
21 | +JustAPI solves this by implementing these critical paths in Rust using: | |
22 | + | |
23 | +- **hyper** — HTTP/1.1 and HTTP/2 implementation | |
24 | +- **tokio** — asynchronous runtime | |
25 | +- **rustls** — TLS termination without OpenSSL | |
26 | +- **matchit** — radix-trie routing | |
27 | +- **PyO3** — seamless Python ↔ Rust boundary | |
28 | + | |
29 | +## Design Philosophy | |
30 | + | |
31 | +1. **FastAPI-compatible.** Existing FastAPI apps migrate with minimal changes. | |
32 | +2. **Rust-first.** If it can be in Rust, it must be in Rust. Python is for application logic only. | |
33 | +3. **Production-ready from day one.** Built-in auth, rate limiting, circuit breakers, and observability. | |
34 | +4. **Honest performance claims.** Every benchmark number in the docs is reproducible on the documented hardware. | |
35 | + | |
36 | +## Performance History | |
37 | + | |
38 | +| Milestone | Date | Result | | |
39 | +|-----------|------|--------| | |
40 | +| Phase 3 | 2026-07 | 752k req/s hello-world | | |
41 | +| Phase 20 | 2026-07 | Beat FastAPI on all benchmarks | | |
42 | +| Phase 25 | 2026-07 | 1M+ RPS tuning | | |
43 | +| Hot-path optimization | 2026-07-13 | 60k rps Python path, 724k rps native | | |
44 | +| Head-to-head vs Robyn | 2026-07-13 | JustAPI ×1.54 faster | | |
45 | + | |
46 | +## Architecture | |
47 | + | |
48 | +``` | |
49 | +Kernel → epoll/io_uring | |
50 | + → Connection Manager (tokio) | |
51 | + → TLS (rustls) | |
52 | + → HTTP parse (hyper) | |
53 | + → Router (matchit) | |
54 | + → Middleware chain (Rust: auth, CORS, rate-limit) | |
55 | + → Python boundary (PyO3) | |
56 | + → Application code (async Python) | |
57 | + → Rust serializer (serde_json / simd-json) | |
58 | + → Response write → Socket | |
59 | +``` | |
60 | + | |
61 | +## See Also | |
62 | + | |
63 | +- [Alternatives, Inspiration & Comparisons](/resources/external-links/) | |
64 | +- [History, Design & Future](/resources/about/) | |
65 | +- [Benchmarks & Performance](/reference/adr-index/) | |
66 | +- [Repository Management](/contributing/coding-standards/) | |
0 | 67 | |
▼docs_site/src/content/docs/resources/external-links.md
+52-0
| @@ -0,0 +1,52 @@ | ||
1 | +--- | |
2 | +title: External Links & Ecosystem | |
3 | +description: Related projects, community tools, and ecosystem around JustAPI. | |
4 | +keywords: [JustAPI, ecosystem, community, related projects, integrations] | |
5 | +--- | |
6 | + | |
7 | +## Core Dependencies | |
8 | + | |
9 | +| Project | Purpose | Link | | |
10 | +|---------|---------|------| | |
11 | +| **Pydantic** | Data validation and serialization | [pydantic.dev](https://pydantic.dev/) | | |
12 | +| **Starlette** | ASGI toolkit (used by JustAPI's shim) | [starlette.io](https://www.starlette.io/) | | |
13 | +| **PyO3** | Python ↔ Rust FFI | [pyo3.rs](https://pyo3.rs/) | | |
14 | +| **maturin** | Build Rust Python packages | [maturin.rs](https://www.maturin.rs/) | | |
15 | + | |
16 | +## Related Rust-Powered Frameworks | |
17 | + | |
18 | +| Framework | What It Does | | |
19 | +|-----------|-------------| | |
20 | +| **Robyn** | Rust runtime, decorator API | | |
21 | +| **Granian** | Rust ASGI server | | |
22 | +| **Litestar** | Python-native ASGI framework | | |
23 | +| **FastAPI** | Python web framework (the compatibility target) | | |
24 | +| **Typer** | CLI framework (FastAPI for CLIs) | | |
25 | + | |
26 | +## Community | |
27 | + | |
28 | +- **GitHub**: [github.com/swadhinbiswas/JustAPI](https://github.com/swadhinbiswas/JustAPI) | |
29 | +- **Stack Overflow**: [tag: justapi](https://stackoverflow.com/questions/tagged/justapi) | |
30 | +- **Issues**: [GitHub Issues](https://github.com/swadhinbiswas/JustAPI/issues) | |
31 | + | |
32 | +## Ecosystem | |
33 | + | |
34 | +### Database Drivers | |
35 | +- **aiosqlite** — async SQLite | |
36 | +- **asyncpg** — async PostgreSQL | |
37 | +- **aiomysql** — async MySQL | |
38 | + | |
39 | +### Serialization | |
40 | +- **orjson** — fast JSON (JustAPI has built-in `orjson` feature) | |
41 | +- **simd-json** — SIMD-accelerated JSON | |
42 | + | |
43 | +### Testing | |
44 | +- **pytest** — Python testing framework | |
45 | +- **httpx** — async HTTP client for testing | |
46 | +- **criterion** — Rust benchmarking | |
47 | + | |
48 | +### Deployment | |
49 | +- **uvicorn** — ASGI server (JustAPI's ASGI shim) | |
50 | +- **gunicorn** — pre-fork server | |
51 | +- **Docker** — container deployment | |
52 | +- **Kubernetes** — orchestration | |
0 | 53 | |
▼docs_site/src/content/docs/resources/help.md
+65-0
| @@ -0,0 +1,65 @@ | ||
1 | +--- | |
2 | +title: Help & Support | |
3 | +description: Where to get help with JustAPI — GitHub Issues, community support, and troubleshooting. | |
4 | +keywords: [JustAPI, help, support, GitHub, community, troubleshooting] | |
5 | +--- | |
6 | + | |
7 | +## GitHub Issues | |
8 | + | |
9 | +For bug reports and feature requests: | |
10 | + | |
11 | +**[GitHub Issues](https://github.com/swadhinbiswas/JustAPI/issues)** | |
12 | + | |
13 | +When reporting a bug, include: | |
14 | +- Python version (`python --version`) | |
15 | +- JustAPI version (`python -c "import justapi; print(justapi.__version__)"`) | |
16 | +- Operating system | |
17 | +- Minimal code to reproduce the issue | |
18 | +- Full error traceback | |
19 | + | |
20 | +## Stack Overflow | |
21 | + | |
22 | +For how-to questions, tag your question with `justapi`: | |
23 | + | |
24 | +**[Stack Overflow: justapi](https://stackoverflow.com/questions/tagged/justapi)** | |
25 | + | |
26 | +## GitHub Discussions | |
27 | + | |
28 | +For general questions, ideas, and community conversation: | |
29 | + | |
30 | +**[GitHub Discussions](https://github.com/swadhinbiswas/JustAPI/discussions)** | |
31 | + | |
32 | +## Security Vulnerabilities | |
33 | + | |
34 | +Do **not** open a public GitHub issue for security vulnerabilities. Instead, follow the process in the [Security Policy](/security/policy/). | |
35 | + | |
36 | +## Troubleshooting | |
37 | + | |
38 | +### ImportError: No module named '_justapi' | |
39 | + | |
40 | +The Rust extension wasn't built. Run `maturin develop --release`. | |
41 | + | |
42 | +### `pip install` fails with build error | |
43 | + | |
44 | +Ensure Rust is installed (`rustup --version`). Or use a pre-built wheel: | |
45 | + | |
46 | +```bash | |
47 | +pip install --only-binary=justapi justapi | |
48 | +``` | |
49 | + | |
50 | +### Handler returns validation error for valid data | |
51 | + | |
52 | +Check that your Pydantic model fields match the request body exactly. All fields without defaults are required. | |
53 | + | |
54 | +### Port already in use | |
55 | + | |
56 | +```bash | |
57 | +# Kill process on port 8000 | |
58 | +lsof -ti:8000 | xargs kill -9 | |
59 | +``` | |
60 | + | |
61 | +Or use a different port: | |
62 | + | |
63 | +```python | |
64 | +app.run("127.0.0.1", 9000) | |
65 | +``` | |
0 | 66 | |
▼docs_site/src/content/docs/tutorials/async-await.md
+138-0
| @@ -0,0 +1,138 @@ | ||
1 | +--- | |
2 | +title: Concurrency and Async / Await | |
3 | +description: Understanding async and await in Python and when to use async def vs def in JustAPI handlers. | |
4 | +keywords: [JustAPI, async, await, concurrency, Python async, FastAPI alternative] | |
5 | +--- | |
6 | + | |
7 | +JustAPI supports both regular functions (`def`) and asynchronous functions (`async def`) as request handlers. This page explains the difference and when to use each. | |
8 | + | |
9 | +## Synchronous vs Asynchronous | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +# Synchronous handler (def) | |
17 | +@app.get("/sync") | |
18 | +def sync_handler(request): | |
19 | + return {"message": "Synchronous"} | |
20 | + | |
21 | +# Asynchronous handler (async def) | |
22 | +@app.get("/async") | |
23 | +async def async_handler(request): | |
24 | + return {"message": "Asynchronous"} | |
25 | +``` | |
26 | + | |
27 | +Both work identically from the user's perspective. The difference is internal. | |
28 | + | |
29 | +### `def` — Synchronous | |
30 | + | |
31 | +JustAPI runs synchronous handlers in a dedicated thread pool so they don't block the Rust event loop. This is handled automatically by the framework. | |
32 | + | |
33 | +Use `def` when your handler does CPU-bound work or calls blocking libraries: | |
34 | + | |
35 | +```python | |
36 | +import time | |
37 | + | |
38 | +@app.get("/cpu-work") | |
39 | +def cpu_work(request): | |
40 | + # Simulate CPU-bound work | |
41 | + result = sum(i * i for i in range(10_000)) | |
42 | + return {"result": result} | |
43 | +``` | |
44 | + | |
45 | +### `async def` — Asynchronous | |
46 | + | |
47 | +Async handlers run directly on the Rust async runtime. They can `await` other async operations without blocking. | |
48 | + | |
49 | +Use `async def` when your handler does I/O-bound work (network calls, database queries, file I/O): | |
50 | + | |
51 | +```python | |
52 | +import httpx | |
53 | + | |
54 | +@app.get("/fetch") | |
55 | +async def fetch_data(request): | |
56 | + async with httpx.AsyncClient() as client: | |
57 | + response = await client.get("https://api.example.com/data") | |
58 | + return response.json() | |
59 | +``` | |
60 | + | |
61 | +## When to Use Which | |
62 | + | |
63 | +| Scenario | Use | Why | | |
64 | +|----------|-----|-----| | |
65 | +| Calling a database ORM (sqlx, SQLAlchemy async) | `async def` | Avoids blocking the runtime | | |
66 | +| HTTP calls to external APIs | `async def` | Non-blocking I/O | | |
67 | +| Simple data transformation | `def` | Cleaner code, no async overhead | | |
68 | +| CPU-bound computation | `def` | Thread pool handles it | | |
69 | +| Mixed I/O + computation | `async def` | Better concurrency | | |
70 | +| Using blocking libraries | `def` | Thread pool prevents blocking | | |
71 | + | |
72 | +:::tip | |
73 | +If you're unsure, use `async def`. It's the modern default and works for all cases. Only use `def` if you're calling blocking code directly. | |
74 | +::: | |
75 | + | |
76 | +## JustAPI's Concurrency Model | |
77 | + | |
78 | +JustAPI uses a Rust async runtime (tokio) for all request handling. The Python handler boundary is designed to avoid GIL contention: | |
79 | + | |
80 | +- **`async def`** handlers run on the tokio async runtime directly. | |
81 | +- **`def`** handlers run in a dedicated Python worker thread pool (GIL pool). | |
82 | +- The native fast path (`native=True`) bypasses Python entirely — no GIL, no threads. | |
83 | + | |
84 | +This means you get true concurrency for async handlers even on standard CPython, because the GIL is released during Python handler execution. | |
85 | + | |
86 | +## Async Database Access | |
87 | + | |
88 | +```python | |
89 | +import sqlite3 | |
90 | +from justapi import JustAPIApp | |
91 | + | |
92 | +app = JustAPIApp() | |
93 | + | |
94 | +@app.get("/users") | |
95 | +async def list_users(request): | |
96 | + # Use an async database client | |
97 | + import aiosqlite | |
98 | + async with aiosqlite.connect("users.db") as db: | |
99 | + async with db.execute("SELECT * FROM users") as cursor: | |
100 | + users = await cursor.fetchall() | |
101 | + return {"users": users} | |
102 | +``` | |
103 | + | |
104 | +## Parallel Async Operations | |
105 | + | |
106 | +Async handlers can run multiple I/O operations concurrently: | |
107 | + | |
108 | +```python | |
109 | +import httpx | |
110 | + | |
111 | +@app.get("/aggregate") | |
112 | +async def aggregate(request): | |
113 | + async with httpx.AsyncClient() as client: | |
114 | + # Run both requests concurrently | |
115 | + users_task = client.get("https://api.example.com/users") | |
116 | + items_task = client.get("https://api.example.com/items") | |
117 | + | |
118 | + users_response = await users_task | |
119 | + items_response = await items_task | |
120 | + | |
121 | + return { | |
122 | + "users": users_response.json(), | |
123 | + "items": items_response.json(), | |
124 | + } | |
125 | +``` | |
126 | + | |
127 | +## Summary | |
128 | + | |
129 | +- Use `def` for synchronous, blocking, or CPU-bound work. | |
130 | +- Use `async def` for I/O-bound work that benefits from concurrency. | |
131 | +- JustAPI handles both correctly — you don't need to worry about blocking the runtime. | |
132 | +- For maximum throughput, consider the native fast path (`native=True`) which bypasses Python entirely. | |
133 | + | |
134 | +## See Also | |
135 | + | |
136 | +- [First Steps](/tutorials/hello-world/) — your first JustAPI app | |
137 | +- [Python Types Intro](/tutorials/python-types/) — type hints in JustAPI | |
138 | +- [Background Tasks](/tutorials/background-tasks/) — post-response async work | |
0 | 139 | |
▼docs_site/src/content/docs/tutorials/body-updates.md
+63-0
| @@ -0,0 +1,63 @@ | ||
1 | +--- | |
2 | +title: Body — Updates | |
3 | +description: Implement partial updates (PATCH) with Pydantic models in JustAPI. | |
4 | +keywords: [JustAPI, PATCH, partial update, body updates, Pydantic] | |
5 | +--- | |
6 | + | |
7 | +## PUT vs PATCH | |
8 | + | |
9 | +- **PUT** — full replacement. The client sends the entire resource. | |
10 | +- **PATCH** — partial update. The client sends only the fields to change. | |
11 | + | |
12 | +## Full Update with PUT | |
13 | + | |
14 | +```python | |
15 | +from pydantic import BaseModel | |
16 | +from justapi import JustAPIApp | |
17 | + | |
18 | +class Item(BaseModel): | |
19 | + name: str | |
20 | + price: float | |
21 | + description: str = "" | |
22 | + | |
23 | +app = JustAPIApp() | |
24 | + | |
25 | +@app.put("/items/{item_id}") | |
26 | +def update_item(item_id: int, item: Item): | |
27 | + return {"item_id": item_id, **item.model_dump()} | |
28 | +``` | |
29 | + | |
30 | +## Partial Update with PATCH | |
31 | + | |
32 | +Use `exclude_unset=True` to get only the fields the client explicitly sent: | |
33 | + | |
34 | +```python | |
35 | +@app.patch("/items/{item_id}") | |
36 | +def partial_update(item_id: int, item: Item): | |
37 | + update_data = item.model_dump(exclude_unset=True) | |
38 | + # update_data only contains fields the client sent | |
39 | + return {"item_id": item_id, "updates": update_data} | |
40 | +``` | |
41 | + | |
42 | +If the client sends `{"name": "New Name"}`, only `name` is in `update_data`. | |
43 | + | |
44 | +## Optional Model | |
45 | + | |
46 | +```python | |
47 | +from typing import Optional | |
48 | + | |
49 | +class ItemUpdate(BaseModel): | |
50 | + name: Optional[str] = None | |
51 | + price: Optional[float] = None | |
52 | + description: Optional[str] = None | |
53 | + | |
54 | +@app.patch("/items/{item_id}") | |
55 | +def partial_update(item_id: int, item: ItemUpdate): | |
56 | + update_data = item.model_dump(exclude_unset=True) | |
57 | + return {"item_id": item_id, "updates": update_data} | |
58 | +``` | |
59 | + | |
60 | +## See Also | |
61 | + | |
62 | +- [Request Body](/tutorials/request-body/) — full request body usage | |
63 | +- [Body — Multiple Parameters](/tutorials/body-multiple-params/) — mixing parameter sources | |
0 | 64 | |
▼docs_site/src/content/docs/tutorials/cookie-params.md
+53-0
| @@ -0,0 +1,53 @@ | ||
1 | +--- | |
2 | +title: Cookie Parameters | |
3 | +description: Read and validate HTTP cookies in JustAPI. | |
4 | +keywords: [JustAPI, cookie parameters, cookies, session, HTTP cookies] | |
5 | +--- | |
6 | + | |
7 | +## Basic Cookie Parameters | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, Cookie | |
11 | + | |
12 | +app = JustAPIApp() | |
13 | + | |
14 | +@app.get("/check") | |
15 | +def check_session(session_id: str = Cookie(...)): | |
16 | + return {"session_id": session_id} | |
17 | +``` | |
18 | + | |
19 | +## Required vs Optional | |
20 | + | |
21 | +```python | |
22 | +@app.get("/check") | |
23 | +def check( | |
24 | + session_id: str = Cookie(...), # required | |
25 | + language: str = Cookie("en"), # optional, defaults to "en" | |
26 | + theme: str = Cookie(None), # optional, defaults to None | |
27 | +): | |
28 | + return {"session_id": session_id, "language": language, "theme": theme} | |
29 | +``` | |
30 | + | |
31 | +## Cookie Models | |
32 | + | |
33 | +```python | |
34 | +from pydantic import BaseModel | |
35 | +from justapi import JustAPIApp, Cookie | |
36 | + | |
37 | +class Cookies(BaseModel): | |
38 | + session_id: str | |
39 | + language: str = "en" | |
40 | + theme: str = "light" | |
41 | + | |
42 | +app = JustAPIApp() | |
43 | + | |
44 | +@app.get("/check") | |
45 | +def check(cookies: Cookies = Cookie(...)): | |
46 | + return cookies.model_dump() | |
47 | +``` | |
48 | + | |
49 | +## See Also | |
50 | + | |
51 | +- [Header Parameters](/tutorials/header-params/) — reading HTTP headers | |
52 | +- [Query Parameters](/tutorials/query-params/) — URL query parameters | |
53 | +- [Security](/tutorials/security/first-steps/) — authentication with cookies | |
0 | 54 | |
▼docs_site/src/content/docs/tutorials/cors.md
+77-0
| @@ -0,0 +1,77 @@ | ||
1 | +--- | |
2 | +title: CORS (Cross-Origin Resource Sharing) | |
3 | +description: Configure CORS headers in JustAPI to allow requests from other domains. | |
4 | +keywords: [JustAPI, CORS, cross-origin, security, browser] | |
5 | +--- | |
6 | + | |
7 | +## Basic CORS | |
8 | + | |
9 | +CORS (Cross-Origin Resource Sharing) allows browsers to make requests to your API from different domains. Without CORS, browsers block these requests. | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +app.add_middleware( | |
17 | + CORSMiddleware, | |
18 | + allow_origins=["https://example.com"], | |
19 | + allow_credentials=True, | |
20 | + allow_methods=["*"], | |
21 | + allow_headers=["*"], | |
22 | +) | |
23 | +``` | |
24 | + | |
25 | +## Permissive CORS (Development) | |
26 | + | |
27 | +For local development, allow all origins: | |
28 | + | |
29 | +```python | |
30 | +from justapi import JustAPIApp | |
31 | +from starlette.middleware.cors import CORSMiddleware | |
32 | + | |
33 | +app = JustAPIApp() | |
34 | + | |
35 | +app.add_middleware( | |
36 | + CORSMiddleware, | |
37 | + allow_origins=["*"], | |
38 | + allow_methods=["*"], | |
39 | + allow_headers=["*"], | |
40 | +) | |
41 | +``` | |
42 | + | |
43 | +:::warning | |
44 | +Never use `allow_origins=["*"]` in production. It defeats the purpose of CORS restrictions. | |
45 | +::: | |
46 | + | |
47 | +## Restrictive CORS (Production) | |
48 | + | |
49 | +In production, specify exactly which origins are allowed: | |
50 | + | |
51 | +```python | |
52 | +app.add_middleware( | |
53 | + CORSMiddleware, | |
54 | + allow_origins=[ | |
55 | + "https://app.example.com", | |
56 | + "https://admin.example.com", | |
57 | + ], | |
58 | + allow_credentials=True, | |
59 | + allow_methods=["GET", "POST", "PUT", "DELETE"], | |
60 | + allow_headers=["Authorization", "Content-Type"], | |
61 | +) | |
62 | +``` | |
63 | + | |
64 | +## Options | |
65 | + | |
66 | +| Parameter | Type | Description | | |
67 | +|-----------|------|-------------| | |
68 | +| `allow_origins` | `list[str]` | Allowed origins (`["*"]` for all) | | |
69 | +| `allow_methods` | `list[str]` | HTTP methods to allow | | |
70 | +| `allow_headers` | `list[str]` | Request headers to allow | | |
71 | +| `allow_credentials` | `bool` | Allow cookies and auth headers | | |
72 | +| `expose_headers` | `list[str]` | Headers the browser can read | | |
73 | + | |
74 | +## See Also | |
75 | + | |
76 | +- [Middleware](/tutorials/middleware/) — general middleware usage | |
77 | +- [Security Policy](/security/policy/) — security best practices | |
0 | 78 | |
▼docs_site/src/content/docs/tutorials/debugging.md
+88-0
| @@ -0,0 +1,88 @@ | ||
1 | +--- | |
2 | +title: Debugging | |
3 | +description: Debug JustAPI applications with detailed error messages, logging, and common troubleshooting patterns. | |
4 | +keywords: [JustAPI, debugging, errors, logging, troubleshooting] | |
5 | +--- | |
6 | + | |
7 | +## Error Messages | |
8 | + | |
9 | +JustAPI returns structured error responses for validation failures: | |
10 | + | |
11 | +```json | |
12 | +{ | |
13 | + "detail": [ | |
14 | + { | |
15 | + "type": "int_parsing", | |
16 | + "loc": ["path", "item_id"], | |
17 | + "msg": "Input should be a valid integer", | |
18 | + "input": "foo" | |
19 | + } | |
20 | + ] | |
21 | +} | |
22 | +``` | |
23 | + | |
24 | +The `detail` array contains every validation error with: | |
25 | +- `type` — error category | |
26 | +- `loc` — where the error occurred (`["path", "body", "query", "header"]`) | |
27 | +- `msg` — human-readable message | |
28 | +- `input` — the value that was rejected | |
29 | + | |
30 | +## Debug Mode | |
31 | + | |
32 | +```python | |
33 | +app = JustAPIApp(debug=True) | |
34 | +``` | |
35 | + | |
36 | +Debug mode enables detailed tracebacks in responses. | |
37 | + | |
38 | +:::warning | |
39 | +Never use `debug=True` in production. It exposes internal details. | |
40 | +::: | |
41 | + | |
42 | +## Common Errors | |
43 | + | |
44 | +### 422 Validation Error | |
45 | + | |
46 | +```python | |
47 | +@app.get("/items/{item_id}") | |
48 | +def get_item(item_id: int): | |
49 | + return {"item_id": item_id} | |
50 | +``` | |
51 | + | |
52 | +`GET /items/foo` returns 422 because `"foo"` is not a valid `int`. | |
53 | + | |
54 | +**Fix:** Ensure the URL value matches the declared type. | |
55 | + | |
56 | +### 404 Not Found | |
57 | + | |
58 | +`GET /nonexistent` returns 404 with no body. | |
59 | + | |
60 | +**Fix:** Check your route path and method. | |
61 | + | |
62 | +### 405 Method Not Allowed | |
63 | + | |
64 | +`POST /items` when only `@app.get("/items")` is defined returns 405. | |
65 | + | |
66 | +**Fix:** Add the missing HTTP method decorator. | |
67 | + | |
68 | +## Logging | |
69 | + | |
70 | +Use standard Python logging with JustAPI: | |
71 | + | |
72 | +```python | |
73 | +import logging | |
74 | + | |
75 | +logging.basicConfig(level=logging.INFO) | |
76 | +logger = logging.getLogger(__name__) | |
77 | + | |
78 | +@app.get("/items") | |
79 | +def list_items(): | |
80 | + logger.info("Listing items") | |
81 | + return {"items": []} | |
82 | +``` | |
83 | + | |
84 | +## See Also | |
85 | + | |
86 | +- [Testing](/tutorials/testing/) — automated tests | |
87 | +- [Error Codes](/reference/error-codes/) — complete error code reference | |
88 | +- [Security Policy](/security/policy/) — reporting security issues | |
0 | 89 | |
▼docs_site/src/content/docs/tutorials/dependencies/classes-as-dependencies/index.md
+49-0
| @@ -0,0 +1,49 @@ | ||
1 | +--- | |
2 | +title: Classes as Dependencies | |
3 | +description: Use Python classes as dependencies in JustAPI for encapsulated logic with state. | |
4 | +keywords: [JustAPI, dependencies, classes, dependency injection] | |
5 | +--- | |
6 | + | |
7 | +## Callable Dependencies as Classes | |
8 | + | |
9 | +Instead of plain functions, use classes as dependencies when you need to encapsulate state: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Depends | |
13 | + | |
14 | +class PaginationParams: | |
15 | + def __init__(self, skip: int = 0, limit: int = 10): | |
16 | + self.skip = skip | |
17 | + self.limit = limit | |
18 | + self.offset = skip * limit # computed property | |
19 | + | |
20 | +@app.get("/items/") | |
21 | +def list_items(pagination: PaginationParams = Depends()): | |
22 | + return {"skip": pagination.skip, "limit": pagination.limit, "offset": pagination.offset} | |
23 | +``` | |
24 | + | |
25 | +JustAPI calls `PaginationParams(skip=..., limit=...)` automatically — the parameters come from query params. | |
26 | + | |
27 | +## Class with Dependencies | |
28 | + | |
29 | +```python | |
30 | +class DBSession: | |
31 | + def __init__(self, db_url: str): | |
32 | + self.db_url = db_url | |
33 | + # In production, create connection pool | |
34 | + | |
35 | + def execute(self, query: str): | |
36 | + return [] | |
37 | + | |
38 | +def get_db(): | |
39 | + return DBSession("sqlite:///app.db") | |
40 | + | |
41 | +@app.get("/users/") | |
42 | +def list_users(db: DBSession = Depends(get_db)): | |
43 | + return db.execute("SELECT * FROM users") | |
44 | +``` | |
45 | + | |
46 | +## See Also | |
47 | + | |
48 | +- [Sub-dependencies](/tutorials/dependencies/sub-dependencies/) — dependencies of dependencies | |
49 | +- [Dependencies with yield](/tutorials/dependencies/dependencies-with-yield/) — cleanup patterns | |
0 | 50 | |
▼docs_site/src/content/docs/tutorials/dependencies/dependencies-in-path-ops/index.md
+38-0
| @@ -0,0 +1,38 @@ | ||
1 | +--- | |
2 | +title: Dependencies in Path Operation Decorators | |
3 | +description: Declare dependencies directly in the route decorator instead of as function parameters. | |
4 | +keywords: [JustAPI, dependencies, route decorator, dependency injection] | |
5 | +--- | |
6 | + | |
7 | +## Decorator-Level Dependencies | |
8 | + | |
9 | +You can declare dependencies in the decorator itself — they won't appear as function parameters: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Depends | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +def verify_api_key(): | |
17 | + api_key = request.headers.get("X-API-Key") | |
18 | + if api_key != "secret-key": | |
19 | + raise HTTPException(status_code=403) | |
20 | + return True | |
21 | + | |
22 | +@app.get("/items/", dependencies=[Depends(verify_api_key)]) | |
23 | +def list_items(): | |
24 | + return {"items": []} | |
25 | +``` | |
26 | + | |
27 | +The `verify_api_key` runs before `list_items`, but `list_items` doesn't receive the result as a parameter. | |
28 | + | |
29 | +## Useful For | |
30 | + | |
31 | +- Authentication checks where the handler doesn't need the result | |
32 | +- Logging or rate limiting | |
33 | +- Cross-cutting concerns that apply to many routes | |
34 | + | |
35 | +## See Also | |
36 | + | |
37 | +- [Global Dependencies](/tutorials/dependencies/global-dependencies/) — app-wide deps | |
38 | +- [Security](/tutorials/security/first-steps/) — authentication patterns | |
0 | 39 | |
▼docs_site/src/content/docs/tutorials/dependencies/dependencies-with-yield/index.md
+57-0
| @@ -0,0 +1,57 @@ | ||
1 | +--- | |
2 | +title: Dependencies with yield | |
3 | +description: Use yield dependencies in JustAPI for setup/teardown patterns like database connections. | |
4 | +keywords: [JustAPI, dependencies, yield, setup, teardown, cleanup] | |
5 | +--- | |
6 | + | |
7 | +## Setup/Teardown with yield | |
8 | + | |
9 | +Use `yield` in dependencies to perform cleanup after the request: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Depends | |
13 | + | |
14 | +def get_db_session(): | |
15 | + session = create_session() | |
16 | + try: | |
17 | + yield session | |
18 | + finally: | |
19 | + session.close() | |
20 | + | |
21 | +app = JustAPIApp() | |
22 | + | |
23 | +@app.get("/users/") | |
24 | +def list_users(db = Depends(get_db_session)): | |
25 | + users = db.query("SELECT * FROM users") | |
26 | + return {"users": users} | |
27 | +``` | |
28 | + | |
29 | +The code before `yield` runs before the request. The code after `yield` runs after the request (even if it raises an exception). | |
30 | + | |
31 | +## Database Connection Pool | |
32 | + | |
33 | +```python | |
34 | +def get_db(): | |
35 | + db = DBConnection("sqlite:///app.db") | |
36 | + try: | |
37 | + yield db | |
38 | + finally: | |
39 | + db.disconnect() | |
40 | +``` | |
41 | + | |
42 | +## Multiple Cleanup Steps | |
43 | + | |
44 | +```python | |
45 | +def get_resource(): | |
46 | + resource = acquire_resource() | |
47 | + try: | |
48 | + yield resource | |
49 | + finally: | |
50 | + release_resource(resource) | |
51 | + cleanup_temp_files() | |
52 | +``` | |
53 | + | |
54 | +## See Also | |
55 | + | |
56 | +- [SQL Databases](/tutorials/database-integration/) — database integration | |
57 | +- [Classes as Dependencies](/tutorials/dependencies/classes-as-dependencies/) — class-based deps | |
0 | 58 | |
▼docs_site/src/content/docs/tutorials/dependencies/global-dependencies/index.md
+71-0
| @@ -0,0 +1,71 @@ | ||
1 | +--- | |
2 | +title: Global Dependencies | |
3 | +description: Apply dependencies to every route in your JustAPI application. | |
4 | +keywords: [JustAPI, global dependencies, app-wide, middleware] | |
5 | +--- | |
6 | + | |
7 | +## App-Level Dependencies | |
8 | + | |
9 | +Declare dependencies that apply to all routes: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Depends | |
13 | + | |
14 | +def verify_token(): | |
15 | + token = request.headers.get("Authorization", "") | |
16 | + if not token: | |
17 | + raise HTTPException(status_code=401) | |
18 | + return True | |
19 | + | |
20 | +app = JustAPIApp(dependencies=[Depends(verify_token)]) | |
21 | + | |
22 | +@app.get("/public") | |
23 | +def public_route(): | |
24 | + # verify_token still runs | |
25 | + return {"message": "This requires auth too"} | |
26 | +``` | |
27 | + | |
28 | +## Router-Level Dependencies | |
29 | + | |
30 | +Apply to all routes in a specific router: | |
31 | + | |
32 | +```python | |
33 | +from justapi import APIRouter | |
34 | + | |
35 | +router = APIRouter(dependencies=[Depends(verify_token)]) | |
36 | + | |
37 | +@router.get("/items/") | |
38 | +def list_items(): | |
39 | + return [] | |
40 | + | |
41 | +@router.post("/items/") | |
42 | +def create_item(): | |
43 | + return {} | |
44 | + | |
45 | +app.include_router(router, prefix="/api") | |
46 | +``` | |
47 | + | |
48 | +## Mixing Global + Local | |
49 | + | |
50 | +```python | |
51 | +def require_auth(): | |
52 | + # authentication check | |
53 | + pass | |
54 | + | |
55 | +def require_admin(): | |
56 | + # admin check | |
57 | + pass | |
58 | + | |
59 | +# App-wide auth | |
60 | +app = JustAPIApp(dependencies=[Depends(require_auth)]) | |
61 | + | |
62 | +# This route also requires admin | |
63 | +@app.get("/admin/users", dependencies=[Depends(require_admin)]) | |
64 | +def admin_users(): | |
65 | + return [] | |
66 | +``` | |
67 | + | |
68 | +## See Also | |
69 | + | |
70 | +- [Dependencies in Path Ops](/tutorials/dependencies/dependencies-in-path-ops/) — route-level deps | |
71 | +- [Security](/tutorials/security/first-steps/) — authentication patterns | |
0 | 72 | |
▼docs_site/src/content/docs/tutorials/dependencies/sub-dependencies/index.md
+51-0
| @@ -0,0 +1,51 @@ | ||
1 | +--- | |
2 | +title: Sub-dependencies | |
3 | +description: Chain dependencies in JustAPI where one dependency requires another. | |
4 | +keywords: [JustAPI, sub-dependencies, dependency chain, dependency injection] | |
5 | +--- | |
6 | + | |
7 | +## Chaining Dependencies | |
8 | + | |
9 | +Dependencies can depend on other dependencies. JustAPI resolves the entire chain automatically: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Depends | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +# Level 1: get token from header | |
17 | +def get_token(authorization: str = Header(...)): | |
18 | + return authorization.replace("Bearer ", "") | |
19 | + | |
20 | +# Level 2: decode token to get user | |
21 | +def get_user(token: str = Depends(get_token)): | |
22 | + # In production, decode JWT | |
23 | + return {"user_id": 1, "name": "Alice"} | |
24 | + | |
25 | +# Level 3: use user in handler | |
26 | +@app.get("/profile") | |
27 | +def profile(user: dict = Depends(get_user)): | |
28 | + return {"user": user["name"]} | |
29 | +``` | |
30 | + | |
31 | +JustAPI resolves `get_token` → `get_user` → `profile` automatically. | |
32 | + | |
33 | +## Sharing Dependencies | |
34 | + | |
35 | +```python | |
36 | +def get_db(): | |
37 | + return DatabaseConnection() | |
38 | + | |
39 | +def get_current_user(db: DBSession = Depends(get_db)): | |
40 | + return db.query_user() | |
41 | + | |
42 | +def get_admin_user(user: dict = Depends(get_current_user)): | |
43 | + if user.get("role") != "admin": | |
44 | + raise HTTPException(status_code=403) | |
45 | + return user | |
46 | +``` | |
47 | + | |
48 | +## See Also | |
49 | + | |
50 | +- [Classes as Dependencies](/tutorials/dependencies/classes-as-dependencies/) — class-based deps | |
51 | +- [Global Dependencies](/tutorials/dependencies/global-dependencies/) — app-wide deps | |
0 | 52 | |
▼docs_site/src/content/docs/tutorials/encoder.md
+47-0
| @@ -0,0 +1,47 @@ | ||
1 | +--- | |
2 | +title: JSON Compatible Encoder | |
3 | +description: Convert Pydantic models and complex types to JSON-compatible dictionaries in JustAPI. | |
4 | +keywords: [JustAPI, encoder, jsonable_encoder, serialization, Pydantic] | |
5 | +--- | |
6 | + | |
7 | +## The Problem | |
8 | + | |
9 | +When you need to convert Pydantic models to JSON-compatible dictionaries (for example, before passing to a database driver), you can't just call `model_dump()` — it doesn't handle `datetime`, `UUID`, or other non-JSON types. | |
10 | + | |
11 | +## JustAPI's Encoder | |
12 | + | |
13 | +```python | |
14 | +from justapi import JustAPIApp | |
15 | +from pydantic import BaseModel | |
16 | +from datetime import datetime | |
17 | + | |
18 | +class Event(BaseModel): | |
19 | + name: str | |
20 | + starts: datetime | |
21 | + | |
22 | +app = JustAPIApp() | |
23 | + | |
24 | +@app.post("/events") | |
25 | +def create_event(event: Event): | |
26 | + # Convert to JSON-compatible dict | |
27 | + data = event.model_dump() | |
28 | + # starts is a datetime object — need to serialize it | |
29 | + return {"event_name": data["name"], "starts": data["starts"].isoformat()} | |
30 | +``` | |
31 | + | |
32 | +## Model_dump with mode | |
33 | + | |
34 | +Pydantic v2 provides `model_dump()` with serialization modes: | |
35 | + | |
36 | +```python | |
37 | +@app.post("/events") | |
38 | +def create_event(event: Event): | |
39 | + data = event.model_dump(mode="json") | |
40 | + # data["starts"] is now an ISO string | |
41 | + return data | |
42 | +``` | |
43 | + | |
44 | +## See Also | |
45 | + | |
46 | +- [Request Body](/tutorials/request-body/) — Pydantic models | |
47 | +- [Response Model](/tutorials/response-model/) — filtering output | |
0 | 48 | |
▼docs_site/src/content/docs/tutorials/extra-data-types.md
+91-0
| @@ -0,0 +1,91 @@ | ||
1 | +--- | |
2 | +title: Extra Data Types | |
3 | +description: Advanced Python data types supported by JustAPI — UUID, datetime, Enum, and more. | |
4 | +keywords: [JustAPI, data types, UUID, datetime, Enum, Decimal, extra types] | |
5 | +--- | |
6 | + | |
7 | +Beyond basic types, JustAPI supports all standard library and Pydantic types: | |
8 | + | |
9 | +## UUID | |
10 | + | |
11 | +```python | |
12 | +from uuid import UUID | |
13 | +from justapi import JustAPIApp | |
14 | + | |
15 | +app = JustAPIApp() | |
16 | + | |
17 | +@app.get("/users/{user_id}") | |
18 | +def get_user(user_id: UUID): | |
19 | + return {"user_id": str(user_id)} | |
20 | +``` | |
21 | + | |
22 | +`GET /users/550e8400-e29b-41d4-a716-446655440000` validates the UUID format. | |
23 | + | |
24 | +## datetime and date | |
25 | + | |
26 | +```python | |
27 | +from datetime import datetime, date | |
28 | +from justapi import JustAPIApp | |
29 | + | |
30 | +app = JustAPIApp() | |
31 | + | |
32 | +@app.post("/events") | |
33 | +def create_event(start: datetime, end: date = None): | |
34 | + return {"start": start.isoformat(), "end": end.isoformat() if end else None} | |
35 | +``` | |
36 | + | |
37 | +Accepts ISO 8601 strings: | |
38 | +- `datetime`: `"2026-07-25T10:30:00Z"` | |
39 | +- `date`: `"2026-07-25"` | |
40 | + | |
41 | +## Enum | |
42 | + | |
43 | +```python | |
44 | +from enum import Enum | |
45 | +from justapi import JustAPIApp | |
46 | + | |
47 | +class Priority(str, Enum): | |
48 | + low = "low" | |
49 | + medium = "medium" | |
50 | + high = "high" | |
51 | + | |
52 | +app = JustAPIApp() | |
53 | + | |
54 | +@app.post("/tasks") | |
55 | +def create_task(priority: Priority): | |
56 | + return {"priority": priority.value} | |
57 | +``` | |
58 | + | |
59 | +The docs will show a dropdown with the allowed values. | |
60 | + | |
61 | +## Decimal | |
62 | + | |
63 | +```python | |
64 | +from decimal import Decimal | |
65 | +from justapi import JustAPIApp | |
66 | + | |
67 | +app = JustAPIApp() | |
68 | + | |
69 | +@app.post("/payments") | |
70 | +def process_payment(amount: Decimal): | |
71 | + return {"amount": str(amount)} | |
72 | +``` | |
73 | + | |
74 | +## Literal Types | |
75 | + | |
76 | +```python | |
77 | +from typing import Literal | |
78 | +from justapi import JustAPIApp | |
79 | + | |
80 | +app = JustAPIApp() | |
81 | + | |
82 | +@app.get("/status") | |
83 | +def get_status(status: Literal["active", "inactive"]): | |
84 | + return {"status": status} | |
85 | +``` | |
86 | + | |
87 | +## See Also | |
88 | + | |
89 | +- [Python Types Intro](/tutorials/python-types/) — basic types | |
90 | +- [Query Parameters & String Validations](/tutorials/query-params-str-validations/) — string validation | |
91 | +- [Path Parameters & Numeric Validations](/tutorials/path-params-numeric-validations/) — numeric validation | |
0 | 92 | |
▼docs_site/src/content/docs/tutorials/extra-models.md
+59-0
| @@ -0,0 +1,59 @@ | ||
1 | +--- | |
2 | +title: Extra Models | |
3 | +description: Define multiple related Pydantic models for different API operations in JustAPI. | |
4 | +keywords: [JustAPI, Pydantic, multiple models, inheritance, schemas] | |
5 | +--- | |
6 | + | |
7 | +## Why Multiple Models? | |
8 | + | |
9 | +Different operations need different data shapes: | |
10 | +- **Create** — input model with required fields | |
11 | +- **Read** — output model with ID and timestamps | |
12 | +- **Update** — optional fields for partial updates | |
13 | + | |
14 | +```python | |
15 | +from pydantic import BaseModel | |
16 | +from typing import Optional | |
17 | + | |
18 | +class ItemBase(BaseModel): | |
19 | + name: str | |
20 | + description: Optional[str] = None | |
21 | + | |
22 | +class ItemCreate(ItemBase): | |
23 | + price: float | |
24 | + | |
25 | +class ItemRead(ItemBase): | |
26 | + id: int | |
27 | + in_stock: bool | |
28 | + | |
29 | +class ItemUpdate(BaseModel): | |
30 | + name: Optional[str] = None | |
31 | + price: Optional[float] = None | |
32 | +``` | |
33 | + | |
34 | +## Using Different Models | |
35 | + | |
36 | +```python | |
37 | +from justapi import JustAPIApp | |
38 | + | |
39 | +app = JustAPIApp() | |
40 | + | |
41 | +@app.post("/items", response_model=ItemRead, status_code=201) | |
42 | +def create_item(item: ItemCreate): | |
43 | + # In production, save to database | |
44 | + return {**item.model_dump(), "id": 1, "in_stock": True} | |
45 | + | |
46 | +@app.get("/items/{item_id}", response_model=ItemRead) | |
47 | +def get_item(item_id: int): | |
48 | + return {"id": item_id, "name": "Widget", "price": 9.99, "in_stock": True} | |
49 | + | |
50 | +@app.patch("/items/{item_id}", response_model=ItemRead) | |
51 | +def update_item(item_id: int, item: ItemUpdate): | |
52 | + return {**item.model_dump(), "id": item_id, "in_stock": True} | |
53 | +``` | |
54 | + | |
55 | +## See Also | |
56 | + | |
57 | +- [Response Model](/tutorials/response-model/) — filtering output | |
58 | +- [Body — Nested Models](/tutorials/body-nested-models/) — nested Pydantic models | |
59 | +- [Request Body](/tutorials/request-body/) — Pydantic model usage | |
0 | 60 | |
▼docs_site/src/content/docs/tutorials/form-data.md
+51-0
| @@ -0,0 +1,51 @@ | ||
1 | +--- | |
2 | +title: Form Data | |
3 | +description: Handle HTML form submissions and form data in JustAPI. | |
4 | +keywords: [JustAPI, form data, form submission, HTML forms] | |
5 | +--- | |
6 | + | |
7 | +## Basic Form Data | |
8 | + | |
9 | +Use `Form(...)` to receive form data: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Form | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +@app.post("/login") | |
17 | +def login(username: str = Form(...), password: str = Form(...)): | |
18 | + return {"username": username} | |
19 | +``` | |
20 | + | |
21 | +This accepts `application/x-www-form-urlencoded` requests. | |
22 | + | |
23 | +## Required vs Optional | |
24 | + | |
25 | +```python | |
26 | +@app.post("/items") | |
27 | +def create_item( | |
28 | + name: str = Form(...), # required | |
29 | + description: str = Form(None), # optional | |
30 | +): | |
31 | + return {"name": name, "description": description} | |
32 | +``` | |
33 | + | |
34 | +## Multiple Form Fields | |
35 | + | |
36 | +```python | |
37 | +@app.post("/submit") | |
38 | +def submit_form( | |
39 | + title: str = Form(...), | |
40 | + body: str = Form(...), | |
41 | + tags: str = Form(""), | |
42 | + publish: bool = Form(False), | |
43 | +): | |
44 | + return {"title": title, "body": body, "tags": tags, "publish": publish} | |
45 | +``` | |
46 | + | |
47 | +## See Also | |
48 | + | |
49 | +- [Form Models](/tutorials/form-models/) — Pydantic models for forms | |
50 | +- [Request Files](/tutorials/request-files/) — file uploads | |
51 | +- [Request Body](/tutorials/request-body/) — JSON request bodies | |
0 | 52 | |
▼docs_site/src/content/docs/tutorials/form-models.md
+54-0
| @@ -0,0 +1,54 @@ | ||
1 | +--- | |
2 | +title: Form Models | |
3 | +description: Use Pydantic models for form data validation in JustAPI. | |
4 | +keywords: [JustAPI, form models, Pydantic forms, form validation] | |
5 | +--- | |
6 | + | |
7 | +## Pydantic Models for Forms | |
8 | + | |
9 | +Use `Body(..., embed=True)` with Pydantic models for complex form data: | |
10 | + | |
11 | +```python | |
12 | +from pydantic import BaseModel | |
13 | +from justapi import JustAPIApp, Form | |
14 | + | |
15 | +class LoginForm(BaseModel): | |
16 | + username: str | |
17 | + password: str | |
18 | + remember_me: bool = False | |
19 | + | |
20 | +app = JustAPIApp() | |
21 | + | |
22 | +@app.post("/login") | |
23 | +def login(form: LoginForm = Form(...)): | |
24 | + return {"username": form.username, "remember_me": form.remember_me} | |
25 | +``` | |
26 | + | |
27 | +:::tip | |
28 | +When using Pydantic models with `Form(...)`, the `embed=True` parameter is required by Pydantic to correctly parse the form fields. | |
29 | +::: | |
30 | + | |
31 | +## File + Form Combination | |
32 | + | |
33 | +```python | |
34 | +from justapi import JustAPIApp, Form, UploadFile | |
35 | + | |
36 | +class CreateItemWithImage(BaseModel): | |
37 | + name: str | |
38 | + description: str = "" | |
39 | + | |
40 | +app = JustAPIApp() | |
41 | + | |
42 | +@app.post("/items/") | |
43 | +async def create_item( | |
44 | + item: CreateItemWithImage = Form(...), | |
45 | + image: UploadFile = File(...), | |
46 | +): | |
47 | + return {"item": item.model_dump(), "filename": image.filename} | |
48 | +``` | |
49 | + | |
50 | +## See Also | |
51 | + | |
52 | +- [Form Data](/tutorials/form-data/) — basic form fields | |
53 | +- [Request Files](/tutorials/request-files/) — file uploads | |
54 | +- [Request Body](/tutorials/request-body/) — JSON request bodies | |
0 | 55 | |
▼docs_site/src/content/docs/tutorials/header-params.md
+61-0
| @@ -0,0 +1,61 @@ | ||
1 | +--- | |
2 | +title: Header Parameters | |
3 | +description: Read and validate HTTP header parameters in JustAPI. | |
4 | +keywords: [JustAPI, header parameters, HTTP headers, validation] | |
5 | +--- | |
6 | + | |
7 | +## Basic Header Parameters | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, Header | |
11 | + | |
12 | +app = JustAPIApp() | |
13 | + | |
14 | +@app.get("/items/") | |
15 | +def read_items(x_token: str = Header(...)): | |
16 | + return {"x_token": x_token} | |
17 | +``` | |
18 | + | |
19 | +The header name `X-Token` is automatically converted to `x_token` (underscores). | |
20 | + | |
21 | +## Multiple Headers | |
22 | + | |
23 | +```python | |
24 | +@app.get("/users/") | |
25 | +def read_users( | |
26 | + x_token: str = Header(...), | |
27 | + x_request_id: str = Header(None), | |
28 | +): | |
29 | + return {"x_token": x_token, "x_request_id": x_request_id} | |
30 | +``` | |
31 | + | |
32 | +## Required vs Optional | |
33 | + | |
34 | +- `Header(...)` — required (the `...` means "no default, required") | |
35 | +- `Header(None)` — optional, defaults to `None` | |
36 | +- `Header("default")` — optional, defaults to `"default"` | |
37 | + | |
38 | +## Underscore Conversion | |
39 | + | |
40 | +By default, JustAPI converts dashes to underscores in header names. To disable this: | |
41 | + | |
42 | +```python | |
43 | +@app.get("/items/") | |
44 | +def read_items(custom_header: str = Header(..., convert_underscores=False)): | |
45 | + return {"custom_header": custom_header} | |
46 | +``` | |
47 | + | |
48 | +## Duplicate Headers | |
49 | + | |
50 | +For headers that can repeat (like `Set-Cookie`): | |
51 | + | |
52 | +```python | |
53 | +@app.get("/items/") | |
54 | +def read_items(x_token_list: list[str] = Header(...)): | |
55 | + return {"x_token_list": x_token_list} | |
56 | +``` | |
57 | + | |
58 | +## See Also | |
59 | + | |
60 | +- [Cookie Parameters](/tutorials/cookie-params/) — reading cookies | |
61 | +- [Request Body](/tutorials/request-body/) — JSON request bodies | |
0 | 62 | |
▼docs_site/src/content/docs/tutorials/metadata.md
+71-0
| @@ -0,0 +1,71 @@ | ||
1 | +--- | |
2 | +title: Metadata and Docs URLs | |
3 | +description: Configure OpenAPI metadata, documentation URLs, and API information in JustAPI. | |
4 | +keywords: [JustAPI, metadata, OpenAPI, Swagger, docs URLs, API info] | |
5 | +--- | |
6 | + | |
7 | +## App Metadata | |
8 | + | |
9 | +JustAPI automatically generates OpenAPI 3.1 documentation. Configure the metadata displayed in the docs: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp( | |
15 | + title="My API", | |
16 | + version="1.0.0", | |
17 | + description="A production API built with JustAPI", | |
18 | + summary="My API summary", | |
19 | + contact={"name": "Support", "email": "[email protected]"}, | |
20 | + license_info={"name": "MIT", "url": "https://opensource.org/licenses/MIT"}, | |
21 | +) | |
22 | +``` | |
23 | + | |
24 | +## Documentation URLs | |
25 | + | |
26 | +```python | |
27 | +app = JustAPIApp( | |
28 | + docs_url="/docs", # Swagger UI (default: /docs) | |
29 | + redoc_url="/redoc", # ReDoc (default: /redoc) | |
30 | + openapi_url="/openapi.json", # OpenAPI schema (default: /openapi.json) | |
31 | +) | |
32 | +``` | |
33 | + | |
34 | +Set any URL to `None` to disable that endpoint: | |
35 | + | |
36 | +```python | |
37 | +app = JustAPIApp( | |
38 | + docs_url=None, # disable Swagger UI | |
39 | + redoc_url=None, # disable ReDoc | |
40 | +) | |
41 | +``` | |
42 | + | |
43 | +## Tags | |
44 | + | |
45 | +Tags group related endpoints in the docs: | |
46 | + | |
47 | +```python | |
48 | +@app.get("/users/", tags=["users"]) | |
49 | +def list_users(): | |
50 | + return [] | |
51 | + | |
52 | +@app.post("/items/", tags=["items"]) | |
53 | +def create_item(): | |
54 | + return {} | |
55 | +``` | |
56 | + | |
57 | +Configure tag descriptions in the app constructor: | |
58 | + | |
59 | +```python | |
60 | +app = JustAPIApp( | |
61 | + openapi_tags=[ | |
62 | + {"name": "users", "description": "User management"}, | |
63 | + {"name": "items", "description": "Item catalog"}, | |
64 | + ], | |
65 | +) | |
66 | +``` | |
67 | + | |
68 | +## See Also | |
69 | + | |
70 | +- [FastAPI CLI](/getting-started/cli-scaffolder/) — project scaffolding | |
71 | +- [Configuration Reference](/reference/configuration/) — all config options | |
0 | 72 | |
▼docs_site/src/content/docs/tutorials/path-operation-config.md
+60-0
| @@ -0,0 +1,60 @@ | ||
1 | +--- | |
2 | +title: Path Operation Configuration | |
3 | +description: Configure OpenAPI metadata for path operations — tags, summary, description, deprecated, and more. | |
4 | +keywords: [JustAPI, path operation, OpenAPI, tags, summary, deprecated] | |
5 | +--- | |
6 | + | |
7 | +## Basic Configuration | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp | |
11 | + | |
12 | +app = JustAPIApp() | |
13 | + | |
14 | +@app.get( | |
15 | + "/items/{item_id}", | |
16 | + summary="Get an item", | |
17 | + description="Retrieve a single item by its ID.", | |
18 | + tags=["items"], | |
19 | + response_description="The item", | |
20 | +) | |
21 | +def get_item(item_id: int): | |
22 | + return {"item_id": item_id} | |
23 | +``` | |
24 | + | |
25 | +## Tags | |
26 | + | |
27 | +Tags group endpoints in the docs: | |
28 | + | |
29 | +```python | |
30 | +@app.get("/users/", tags=["users"]) | |
31 | +def list_users(): | |
32 | + return [] | |
33 | + | |
34 | +@app.post("/items/", tags=["items"]) | |
35 | +def create_item(): | |
36 | + return {} | |
37 | +``` | |
38 | + | |
39 | +## Deprecating Endpoints | |
40 | + | |
41 | +```python | |
42 | +@app.get("/old-endpoint", deprecated=True) | |
43 | +def old_endpoint(): | |
44 | + return {"message": "this endpoint is deprecated"} | |
45 | +``` | |
46 | + | |
47 | +## Controlling Doc Visibility | |
48 | + | |
49 | +```python | |
50 | +@app.get("/internal", include_in_schema=False) | |
51 | +def internal_only(): | |
52 | + return {"secret": "data"} | |
53 | +``` | |
54 | + | |
55 | +This hides the endpoint from OpenAPI documentation entirely. | |
56 | + | |
57 | +## See Also | |
58 | + | |
59 | +- [Metadata & Docs URLs](/tutorials/metadata/) — app-level metadata | |
60 | +- [OpenAPI Callbacks & Webhooks](/advanced/openapi-callbacks/) — advanced OpenAPI | |
0 | 61 | |
▼docs_site/src/content/docs/tutorials/path-params.md
+123-0
| @@ -0,0 +1,123 @@ | ||
1 | +--- | |
2 | +title: Path Parameters | |
3 | +description: Declare and validate path parameters in JustAPI using Python type hints. | |
4 | +keywords: [JustAPI, path parameters, URL parameters, validation, FastAPI alternative] | |
5 | +--- | |
6 | + | |
7 | +Path parameters (also called URL variables) are values embedded in the URL path. JustAPI extracts them automatically based on your function signature. | |
8 | + | |
9 | +## Basic Path Parameters | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +@app.get("/users/{user_id}") | |
17 | +def get_user(user_id: int): | |
18 | + return {"user_id": user_id} | |
19 | + | |
20 | +@app.get("/items/{item_id}") | |
21 | +def get_item(item_id: str): | |
22 | + return {"item_id": item_id} | |
23 | +``` | |
24 | + | |
25 | +The `{user_id}` and `{item_id}` in the path are matched to the function parameters. JustAPI validates the type automatically: | |
26 | + | |
27 | +- `/users/42` works — `user_id` is `42` (int) | |
28 | +- `/users/foo` returns a validation error — `"foo"` is not a valid integer | |
29 | + | |
30 | +## Data Conversion | |
31 | + | |
32 | +JustAPI converts path parameters from strings to the declared Python type: | |
33 | + | |
34 | +```python | |
35 | +@app.get("/items/{item_id}") | |
36 | +def get_item(item_id: int): | |
37 | + # item_id is an int, not a string | |
38 | + return {"item_id": item_id, "double": item_id * 2} | |
39 | +``` | |
40 | + | |
41 | +- `/items/3` returns `{"item_id": 3, "double": 6}` | |
42 | +- `/items/3.14` returns a validation error — `"3.14"` is not a valid `int` | |
43 | + | |
44 | +## Supported Types | |
45 | + | |
46 | +| Type | Example URL | Value | | |
47 | +|------|-------------|-------| | |
48 | +| `int` | `/items/42` | `42` | | |
49 | +| `float` | `/items/3.14` | `3.14` | | |
50 | +| `str` | `/items/foo` | `"foo"` | | |
51 | +| `bool` | `/items/true` | `True` | | |
52 | +| `UUID` | `/items/550e8400-...` | `UUID(...)` | | |
53 | +| `Enum` | `/models/alexnet` | `ModelName.alexnet` | | |
54 | + | |
55 | +## Predefined Values with Enum | |
56 | + | |
57 | +Restrict a path parameter to a fixed set of values using Python `Enum`: | |
58 | + | |
59 | +```python | |
60 | +from enum import Enum | |
61 | +from justapi import JustAPIApp | |
62 | + | |
63 | +class ModelName(str, Enum): | |
64 | + alexnet = "alexnet" | |
65 | + resnet = "resnet" | |
66 | + lenet = "lenet" | |
67 | + | |
68 | +app = JustAPIApp() | |
69 | + | |
70 | +@app.get("/models/{model_name}") | |
71 | +def get_model(model_name: ModelName): | |
72 | + if model_name is ModelName.alexnet: | |
73 | + return {"model_name": model_name, "message": "Deep Learning FTW!"} | |
74 | + return {"model_name": model_name, "message": "Have some residuals"} | |
75 | +``` | |
76 | + | |
77 | +The OpenAPI docs will show the available values in a dropdown. | |
78 | + | |
79 | +## Path Parameters Containing Paths | |
80 | + | |
81 | +For parameters that must match a full path (like file paths), use the `:path` converter: | |
82 | + | |
83 | +```python | |
84 | +@app.get("/files/{file_path:path}") | |
85 | +def read_file(file_path: str): | |
86 | + return {"file_path": file_path} | |
87 | +``` | |
88 | + | |
89 | +This matches URLs like `/files/home/user/file.txt`. | |
90 | + | |
91 | +:::note | |
92 | +OpenAPI does not support the `:path` converter. The docs will not document that the parameter should contain a path, but the route will work correctly. | |
93 | +::: | |
94 | + | |
95 | +## Order Matters | |
96 | + | |
97 | +When defining routes, put fixed paths before parameterized paths: | |
98 | + | |
99 | +```python | |
100 | +@app.get("/users/me") | |
101 | +def read_user_me(): | |
102 | + return {"user_id": "the current user"} | |
103 | + | |
104 | +@app.get("/users/{user_id}") | |
105 | +def read_user(user_id: str): | |
106 | + return {"user_id": user_id} | |
107 | +``` | |
108 | + | |
109 | +If `/users/{user_id}` were declared first, it would also match `/users/me`. | |
110 | + | |
111 | +## Recap | |
112 | + | |
113 | +- Use `{parameter_name}` in the path to declare a path parameter. | |
114 | +- Declare the type in your function signature — JustAPI validates and converts it. | |
115 | +- Use `Enum` for predefined values. | |
116 | +- Use `:path` for parameters containing full paths. | |
117 | +- Put fixed routes before parameterized routes. | |
118 | + | |
119 | +## See Also | |
120 | + | |
121 | +- [Path Parameters & Numeric Validations](/tutorials/path-params-numeric-validations/) — add `ge`, `le`, `gt`, `lt` constraints | |
122 | +- [Query Parameters](/tutorials/query-params/) — parameters in the query string | |
123 | +- [Request Body](/tutorials/request-body/) — JSON request bodies | |
0 | 124 | |
▼docs_site/src/content/docs/tutorials/python-types.md
+152-0
| @@ -0,0 +1,152 @@ | ||
1 | +--- | |
2 | +title: Python Types Intro | |
3 | +description: Introduction to Python type hints and how JustAPI uses them for automatic validation, serialization, and OpenAPI documentation. | |
4 | +keywords: [JustAPI, Python types, type hints, Pydantic, validation, FastAPI alternative] | |
5 | +--- | |
6 | + | |
7 | +JustAPI uses standard Python type hints to power request validation, serialization, and auto-generated documentation. This page explains the types JustAPI supports and how they map to API contracts. | |
8 | + | |
9 | +## Basic Types | |
10 | + | |
11 | +JustAPI accepts all standard Python built-in types as function parameters and Pydantic model fields: | |
12 | + | |
13 | +```python | |
14 | +from justapi import JustAPIApp | |
15 | + | |
16 | +app = JustAPIApp() | |
17 | + | |
18 | +@app.get("/users/{user_id}") | |
19 | +def get_user(user_id: int): | |
20 | + return {"user_id": user_id} | |
21 | + | |
22 | +@app.post("/items") | |
23 | +def create_item(name: str, price: float, in_stock: bool = True): | |
24 | + return {"name": name, "price": price, "in_stock": in_stock} | |
25 | +``` | |
26 | + | |
27 | +JustAPI automatically converts the path parameter `user_id` from a string to an `int`. If the value is not a valid integer, the API returns a clear validation error instead of crashing. | |
28 | + | |
29 | +| Type | Example | Validation | | |
30 | +|------|---------|------------| | |
31 | +| `int` | `42` | Rejects non-numeric strings | | |
32 | +| `float` | `3.14` | Rejects non-numeric strings | | |
33 | +| `str` | `"hello"` | Default string | | |
34 | +| `bool` | `True` / `False` | Accepts `true` / `false` / `1` / `0` | | |
35 | +| `bytes` | `b"data"` | Binary data | | |
36 | + | |
37 | +## Collections | |
38 | + | |
39 | +```python | |
40 | +from justapi import JustAPIApp | |
41 | +from pydantic import BaseModel | |
42 | + | |
43 | +app = JustAPIApp() | |
44 | + | |
45 | +@app.get("/filter") | |
46 | +def filter_items(tags: list[str] = None, limit: int = 10): | |
47 | + return {"tags": tags, "limit": limit} | |
48 | +``` | |
49 | + | |
50 | +:::tip | |
51 | +Query parameters like `tags=a&tags=b&tags=c` are automatically parsed into `["a", "b", "c"]`. | |
52 | +::: | |
53 | + | |
54 | +| Type | Use Case | | |
55 | +|------|----------| | |
56 | +| `list[str]` | Multiple query values or JSON array | | |
57 | +| `dict[str, str]` | JSON object with string keys and values | | |
58 | +| `set[str]` | Unique values | | |
59 | +| `tuple[int, str]` | Fixed-length JSON arrays | | |
60 | + | |
61 | +## Optional vs Required | |
62 | + | |
63 | +A parameter is **required** if it has no default value. It is **optional** if it has a default value or uses `Optional` / `None`: | |
64 | + | |
65 | +```python | |
66 | +from typing import Optional | |
67 | +from justapi import JustAPIApp | |
68 | + | |
69 | +app = JustAPIApp() | |
70 | + | |
71 | +@app.get("/search") | |
72 | +def search(q: str, limit: int = 10, tag: Optional[str] = None): | |
73 | + return {"q": q, "limit": limit, "tag": tag} | |
74 | +``` | |
75 | + | |
76 | +- `q: str` — required, no default | |
77 | +- `limit: int = 10` — optional, defaults to `10` | |
78 | +- `tag: Optional[str] = None` — optional, defaults to `None` | |
79 | + | |
80 | +## Pydantic Models | |
81 | + | |
82 | +The most powerful way to define structured data is with Pydantic `BaseModel`: | |
83 | + | |
84 | +```python | |
85 | +from justapi import JustAPIApp | |
86 | +from pydantic import BaseModel | |
87 | +from typing import Optional | |
88 | + | |
89 | +app = JustAPIApp() | |
90 | + | |
91 | +class Item(BaseModel): | |
92 | + name: str | |
93 | + price: float | |
94 | + description: Optional[str] = None | |
95 | + tags: list[str] = [] | |
96 | + | |
97 | +@app.post("/items") | |
98 | +def create_item(item: Item): | |
99 | + return item.model_dump() | |
100 | +``` | |
101 | + | |
102 | +JustAPI reads the request body as JSON, validates it against the model, and passes a typed `Item` object to your handler. Invalid data produces a clear error response with the exact field and issue. | |
103 | + | |
104 | +## Advanced Types | |
105 | + | |
106 | +JustAPI supports all Pydantic and Python standard library types: | |
107 | + | |
108 | +```python | |
109 | +from datetime import datetime, date | |
110 | +from uuid import UUID | |
111 | +from decimal import Decimal | |
112 | +from enum import Enum | |
113 | +from typing import Literal | |
114 | + | |
115 | +class Color(str, Enum): | |
116 | + red = "red" | |
117 | + green = "green" | |
118 | + blue = "blue" | |
119 | + | |
120 | +@app.post("/orders") | |
121 | +def create_order( | |
122 | + order_id: UUID, | |
123 | + created_at: datetime, | |
124 | + color: Color, | |
125 | + amount: Decimal, | |
126 | +): | |
127 | + return {"order_id": str(order_id), "color": color.value} | |
128 | +``` | |
129 | + | |
130 | +| Type | Wire Format | Example | | |
131 | +|------|-------------|---------| | |
132 | +| `datetime` | ISO 8601 string | `"2026-07-25T10:30:00"` | | |
133 | +| `date` | ISO 8601 date | `"2026-07-25"` | | |
134 | +| `UUID` | String | `"550e8400-e29b-41d4-a716-446655440000"` | | |
135 | +| `Decimal` | String or number | `"3.14"` | | |
136 | +| `Enum` | String (value) | `"red"` | | |
137 | +| `Literal["a", "b"]` | One of the literals | `"a"` | | |
138 | + | |
139 | +## Recap | |
140 | + | |
141 | +- JustAPI uses standard Python type hints for validation, serialization, and documentation. | |
142 | +- Built-in types (`int`, `str`, `float`, `bool`) are auto-converted from strings in path/query params. | |
143 | +- `Optional[T]` or `T = None` makes a parameter optional. | |
144 | +- Pydantic `BaseModel` provides structured request body validation with nested models. | |
145 | +- Advanced types (`datetime`, `UUID`, `Decimal`, `Enum`) are fully supported. | |
146 | + | |
147 | +## See Also | |
148 | + | |
149 | +- [First Steps](/tutorials/hello-world/) — your first JustAPI app | |
150 | +- [Path Parameters](/tutorials/path-params/) — working with path parameters | |
151 | +- [Query Parameters](/tutorials/query-params/) — working with query parameters | |
152 | +- [Request Body](/tutorials/request-body/) — request body with Pydantic | |
0 | 153 | |
▼docs_site/src/content/docs/tutorials/query-params.md
+106-0
| @@ -0,0 +1,106 @@ | ||
1 | +--- | |
2 | +title: Query Parameters | |
3 | +description: Declare and validate query parameters in JustAPI. | |
4 | +keywords: [JustAPI, query parameters, URL parameters, FastAPI alternative] | |
5 | +--- | |
6 | + | |
7 | +Query parameters are the key-value pairs after `?` in a URL. JustAPI extracts them automatically from function parameters. | |
8 | + | |
9 | +## Basic Query Parameters | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +@app.get("/items/") | |
17 | +def list_items(skip: int = 0, limit: int = 10): | |
18 | + return {"skip": skip, "limit": limit} | |
19 | +``` | |
20 | + | |
21 | +- `/items/` returns `{"skip": 0, "limit": 10}` | |
22 | +- `/items/?skip=20&limit=50` returns `{"skip": 20, "limit": 50}` | |
23 | + | |
24 | +Any parameter that has a default value is treated as a query parameter. | |
25 | + | |
26 | +## Optional vs Required | |
27 | + | |
28 | +A query parameter is **required** if it has no default value: | |
29 | + | |
30 | +```python | |
31 | +@app.get("/search") | |
32 | +def search(q: str): # required — no default value | |
33 | + return {"query": q} | |
34 | +``` | |
35 | + | |
36 | +`/search` without `?q=...` returns a validation error. | |
37 | + | |
38 | +A query parameter is **optional** if it has a default value: | |
39 | + | |
40 | +```python | |
41 | +@app.get("/search") | |
42 | +def search(q: str = None): # optional — defaults to None | |
43 | + return {"query": q} | |
44 | +``` | |
45 | + | |
46 | +`/search` without `?q=...` works and returns `{"query": null}`. | |
47 | + | |
48 | +## Default Values | |
49 | + | |
50 | +```python | |
51 | +@app.get("/products") | |
52 | +def list_products( | |
53 | + category: str = "all", | |
54 | + min_price: float = 0.0, | |
55 | + max_price: float = 1000.0, | |
56 | + sort: str = "name", | |
57 | + order: str = "asc", | |
58 | +): | |
59 | + return { | |
60 | + "category": category, | |
61 | + "price_range": [min_price, max_price], | |
62 | + "sort": sort, | |
63 | + "order": order, | |
64 | + } | |
65 | +``` | |
66 | + | |
67 | +All parameters are optional with their defaults. Clients only need to override the ones they care about. | |
68 | + | |
69 | +## Type Conversion | |
70 | + | |
71 | +JustAPI converts query parameters from strings to the declared Python type: | |
72 | + | |
73 | +```python | |
74 | +@app.get("/items/{item_id}") | |
75 | +def get_item(item_id: int, verbose: bool = False): | |
76 | + return {"item_id": item_id, "verbose": verbose} | |
77 | +``` | |
78 | + | |
79 | +- `/items/5?verbose=true` — `verbose` is `True` | |
80 | +- `/items/5?verbose=1` — `verbose` is `True` | |
81 | +- `/items/5?verbose=false` — `verbose` is `False` | |
82 | + | |
83 | +## Multiple Values for One Parameter | |
84 | + | |
85 | +Query parameters can repeat: | |
86 | + | |
87 | +```python | |
88 | +@app.get("/filter") | |
89 | +def filter_items(tags: list[str] = None): | |
90 | + return {"tags": tags} | |
91 | +``` | |
92 | + | |
93 | +`/filter?tags=a&tags=b&tags=c` returns `{"tags": ["a", "b", "c"]}`. | |
94 | + | |
95 | +## Recap | |
96 | + | |
97 | +- Parameters with default values are query parameters. | |
98 | +- Parameters without default values are required. | |
99 | +- JustAPI converts types automatically (`str` to `int`, `"true"` to `True`, etc.). | |
100 | +- Use `list[T]` for repeating query parameters. | |
101 | + | |
102 | +## See Also | |
103 | + | |
104 | +- [Query Parameters & String Validations](/tutorials/query-params-str-validations/) — add `min_length`, `max_length`, `pattern` constraints | |
105 | +- [Path Parameters](/tutorials/path-params/) — URL path parameters | |
106 | +- [Request Body](/tutorials/request-body/) — JSON request bodies | |
0 | 107 | |
▼docs_site/src/content/docs/tutorials/request-files.md
+67-0
| @@ -0,0 +1,67 @@ | ||
1 | +--- | |
2 | +title: Request Files | |
3 | +description: Handle file uploads with UploadFile in JustAPI. | |
4 | +keywords: [JustAPI, file upload, UploadFile, multipart, binary] | |
5 | +--- | |
6 | + | |
7 | +## Basic File Upload | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, UploadFile, File | |
11 | + | |
12 | +app = JustAPIApp() | |
13 | + | |
14 | +@app.post("/upload") | |
15 | +async def upload_file(file: UploadFile = File(...)): | |
16 | + content = await file.read() | |
17 | + return { | |
18 | + "filename": file.filename, | |
19 | + "content_type": file.content_type, | |
20 | + "size": len(content), | |
21 | + } | |
22 | +``` | |
23 | + | |
24 | +## Multiple File Upload | |
25 | + | |
26 | +```python | |
27 | +@app.post("/upload-multiple") | |
28 | +async def upload_multiple(files: list[UploadFile] = File(...)): | |
29 | + results = [] | |
30 | + for file in files: | |
31 | + content = await file.read() | |
32 | + results.append({"filename": file.filename, "size": len(content)}) | |
33 | + return {"files": results} | |
34 | +``` | |
35 | + | |
36 | +## Save File to Disk | |
37 | + | |
38 | +```python | |
39 | +import shutil | |
40 | +from pathlib import Path | |
41 | + | |
42 | +UPLOAD_DIR = Path("uploads") | |
43 | +UPLOAD_DIR.mkdir(exist_ok=True) | |
44 | + | |
45 | +@app.post("/upload-save") | |
46 | +async def upload_and_save(file: UploadFile = File(...)): | |
47 | + file_path = UPLOAD_DIR / file.filename | |
48 | + with open(file_path, "wb") as buffer: | |
49 | + shutil.copyfileobj(file.file, buffer) | |
50 | + return {"saved_to": str(file_path)} | |
51 | +``` | |
52 | + | |
53 | +## UploadFile Properties | |
54 | + | |
55 | +| Property | Type | Description | | |
56 | +|----------|------|-------------| | |
57 | +| `filename` | `str` | Original filename | | |
58 | +| `content_type` | `str` | MIME type | | |
59 | +| `file` | `BinaryIO` | File-like object | | |
60 | +| `read()` | `await bytes` | Read entire file | | |
61 | +| `write()` | `await None` | Write data | | |
62 | + | |
63 | +## See Also | |
64 | + | |
65 | +- [Request Forms & Files](/tutorials/request-forms-files/) — forms with files | |
66 | +- [Form Data](/tutorials/form-data/) — form-only submissions | |
67 | +- [UploadFile Reference](/api-reference/uploadfile/) — full API reference | |
0 | 68 | |
▼docs_site/src/content/docs/tutorials/request-forms-files.md
+48-0
| @@ -0,0 +1,48 @@ | ||
1 | +--- | |
2 | +title: Request Forms and Files | |
3 | +description: Handle both form data and file uploads in a single request in JustAPI. | |
4 | +keywords: [JustAPI, form data, file upload, combined, multipart] | |
5 | +--- | |
6 | + | |
7 | +## Combined Form + File | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, UploadFile, File, Form | |
11 | + | |
12 | +app = JustAPIApp() | |
13 | + | |
14 | +@app.post("/items/") | |
15 | +async def create_item( | |
16 | + name: str = Form(...), | |
17 | + description: str = Form(""), | |
18 | + image: UploadFile = File(None), | |
19 | +): | |
20 | + result = {"name": name, "description": description} | |
21 | + if image: | |
22 | + content = await image.read() | |
23 | + result["image_size"] = len(content) | |
24 | + return result | |
25 | +``` | |
26 | + | |
27 | +## Multiple Files + Form Data | |
28 | + | |
29 | +```python | |
30 | +@app.post("/upload/") | |
31 | +async def upload_with_metadata( | |
32 | + title: str = Form(...), | |
33 | + tags: list[str] = Form([]), | |
34 | + files: list[UploadFile] = File(...), | |
35 | +): | |
36 | + return { | |
37 | + "title": title, | |
38 | + "tags": tags, | |
39 | + "file_count": len(files), | |
40 | + "filenames": [f.filename for f in files], | |
41 | + } | |
42 | +``` | |
43 | + | |
44 | +## See Also | |
45 | + | |
46 | +- [Form Data](/tutorials/form-data/) — forms without files | |
47 | +- [Request Files](/tutorials/request-files/) — files without forms | |
48 | +- [Form Models](/tutorials/form-models/) — Pydantic models for forms | |
0 | 49 | |
▼docs_site/src/content/docs/tutorials/response-model.md
+76-0
| @@ -0,0 +1,76 @@ | ||
1 | +--- | |
2 | +title: Response Model — Return Type | |
3 | +description: Control the response schema and filter output fields in JustAPI with response_model. | |
4 | +keywords: [JustAPI, response model, return type, filtering, schema] | |
5 | +--- | |
6 | + | |
7 | +## Basic Response Model | |
8 | + | |
9 | +Declare the response type with `response_model`: | |
10 | + | |
11 | +```python | |
12 | +from pydantic import BaseModel | |
13 | +from justapi import JustAPIApp | |
14 | + | |
15 | +class User(BaseModel): | |
16 | + id: int | |
17 | + name: str | |
18 | + email: str | |
19 | + password: str | |
20 | + | |
21 | +app = JustAPIApp() | |
22 | + | |
23 | +@app.get("/users/{user_id}", response_model=User) | |
24 | +def get_user(user_id: int): | |
25 | + return {"id": user_id, "name": "Alice", "email": "[email protected]", "password": "secret"} | |
26 | +``` | |
27 | + | |
28 | +The `password` field is automatically excluded from the response because it's not declared in the response model. | |
29 | + | |
30 | +## Filtering Response Fields | |
31 | + | |
32 | +```python | |
33 | +class UserOut(BaseModel): | |
34 | + id: int | |
35 | + name: str | |
36 | + | |
37 | +@app.get("/users/{user_id}", response_model=UserOut) | |
38 | +def get_user(user_id: int): | |
39 | + return {"id": user_id, "name": "Alice", "email": "[email protected]"} | |
40 | +``` | |
41 | + | |
42 | +Only `id` and `name` appear in the response. | |
43 | + | |
44 | +## Exclude Fields | |
45 | + | |
46 | +```python | |
47 | +@app.get( | |
48 | + "/users/{user_id}", | |
49 | + response_model=User, | |
50 | + response_model_exclude={"password"}, | |
51 | +) | |
52 | +def get_user(user_id: int): | |
53 | + return {"id": user_id, "name": "Alice", "email": "[email protected]", "password": "secret"} | |
54 | +``` | |
55 | + | |
56 | +## Multiple Response Models | |
57 | + | |
58 | +```python | |
59 | +from typing import Union | |
60 | + | |
61 | +@app.get( | |
62 | + "/users/{user_id}", | |
63 | + response_model=Union[UserOut, dict], | |
64 | + responses={404: {"model": dict}}, | |
65 | +) | |
66 | +def get_user(user_id: int): | |
67 | + if user_id == 0: | |
68 | + return {"error": "not found"} | |
69 | + return {"id": user_id, "name": "Alice"} | |
70 | +``` | |
71 | + | |
72 | +## See Also | |
73 | + | |
74 | +- [Extra Models](/tutorials/extra-models/) — multiple related models | |
75 | +- [Response Status Code](/tutorials/response-status-code/) — custom status codes | |
76 | +- [Custom Response Classes](/advanced/custom-response/) — HTML, stream, file responses | |
0 | 77 | |
▼docs_site/src/content/docs/tutorials/response-status-code.md
+59-0
| @@ -0,0 +1,59 @@ | ||
1 | +--- | |
2 | +title: Response Status Code | |
3 | +description: Set custom HTTP response status codes in JustAPI handlers. | |
4 | +keywords: [JustAPI, status code, HTTP status, response, error handling] | |
5 | +--- | |
6 | + | |
7 | +## Default Status Code | |
8 | + | |
9 | +JustAPI returns `200 OK` by default for successful responses. | |
10 | + | |
11 | +## Custom Status Code | |
12 | + | |
13 | +Use `status_code` in the decorator: | |
14 | + | |
15 | +```python | |
16 | +from justapi import JustAPIApp | |
17 | + | |
18 | +app = JustAPIApp() | |
19 | + | |
20 | +@app.post("/items", status_code=201) | |
21 | +def create_item(): | |
22 | + return {"message": "created"} | |
23 | +``` | |
24 | + | |
25 | +## Common Status Codes | |
26 | + | |
27 | +| Code | Meaning | Use When | | |
28 | +|------|---------|----------| | |
29 | +| 200 | OK | GET, successful PUT/PATCH | | |
30 | +| 201 | Created | Successful POST | | |
31 | +| 204 | No Content | Successful DELETE | | |
32 | +| 301 | Moved Permanently | Redirect | | |
33 | +| 400 | Bad Request | Client error | | |
34 | +| 401 | Unauthorized | Authentication required | | |
35 | +| 403 | Forbidden | Authenticated but not authorized | | |
36 | +| 404 | Not Found | Resource doesn't exist | | |
37 | +| 422 | Unprocessable Entity | Validation error | | |
38 | + | |
39 | +## Returning a Response Directly | |
40 | + | |
41 | +For full control, return a `Response` object: | |
42 | + | |
43 | +```python | |
44 | +from starlette.responses import JSONResponse | |
45 | + | |
46 | +@app.get("/custom") | |
47 | +def custom_response(): | |
48 | + return JSONResponse( | |
49 | + content={"message": "custom"}, | |
50 | + status_code=202, | |
51 | + headers={"X-Custom": "header"}, | |
52 | + ) | |
53 | +``` | |
54 | + | |
55 | +## See Also | |
56 | + | |
57 | +- [Handling Errors](/tutorials/error-handling/) — exception handlers | |
58 | +- [Additional Status Codes](/advanced/response-directly/) — advanced status code patterns | |
59 | +- [Status Codes Reference](/api-reference/routing/) — all available status codes | |
0 | 60 | |
▼docs_site/src/content/docs/tutorials/security/first-steps/index.md
+36-0
| @@ -0,0 +1,36 @@ | ||
1 | +--- | |
2 | +title: Security — First Steps | |
3 | +description: Add basic authentication to a JustAPI route. | |
4 | +keywords: [JustAPI, security, authentication, OAuth2, bearer token] | |
5 | +--- | |
6 | + | |
7 | +## Adding Security to a Route | |
8 | + | |
9 | +Use `Security()` (like `Depends()` but for security) to protect routes: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, Security, Depends | |
13 | +from fastapi.security import OAuth2PasswordBearer | |
14 | + | |
15 | +app = JustAPIApp() | |
16 | + | |
17 | +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") | |
18 | + | |
19 | +def get_current_user(token: str = Security(oauth2_scheme)): | |
20 | + # In production, decode the token | |
21 | + return {"user_id": 1, "name": "Alice"} | |
22 | + | |
23 | +@app.get("/users/me") | |
24 | +def read_users_me(current_user: dict = Security(get_current_user)): | |
25 | + return current_user | |
26 | +``` | |
27 | + | |
28 | +## OAuth2PasswordBearer | |
29 | + | |
30 | +The `tokenUrl` tells the docs where to send login requests. It appears as an "Authorize" button in Swagger UI. | |
31 | + | |
32 | +## See Also | |
33 | + | |
34 | +- [Get Current User](/tutorials/security/get-current-user/) — extracting user from token | |
35 | +- [Simple OAuth2](/tutorials/security/simple-oauth2/) — password-based OAuth2 | |
36 | +- [OAuth2 + JWT](/tutorials/security/oauth2-jwt/) — full JWT implementation | |
0 | 37 | |
▼docs_site/src/content/docs/tutorials/security/get-current-user/index.md
+32-0
| @@ -0,0 +1,32 @@ | ||
1 | +--- | |
2 | +title: Get Current User | |
3 | +description: Extract and validate the current authenticated user from a JWT token in JustAPI. | |
4 | +keywords: [JustAPI, security, current user, JWT, token] | |
5 | +--- | |
6 | + | |
7 | +## Extracting User from Token | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, Security | |
11 | +from fastapi.security import OAuth2PasswordBearer | |
12 | + | |
13 | +app = JustAPIApp() | |
14 | +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") | |
15 | + | |
16 | +async def get_current_user(token: str = Security(oauth2_scheme)): | |
17 | + # Decode JWT and return user | |
18 | + user = decode_token(token) | |
19 | + if user is None: | |
20 | + raise HTTPException(status_code=401, detail="Invalid token") | |
21 | + return user | |
22 | + | |
23 | +@app.get("/users/me") | |
24 | +def read_me(current_user: dict = Security(get_current_user)): | |
25 | + return current_user | |
26 | +``` | |
27 | + | |
28 | +## See Also | |
29 | + | |
30 | +- [Security — First Steps](/tutorials/security/first-steps/) — basic security setup | |
31 | +- [Simple OAuth2](/tutorials/security/simple-oauth2/) — password-based auth | |
32 | +- [OAuth2 + JWT](/tutorials/security/oauth2-jwt/) — full JWT implementation | |
0 | 33 | |
▼docs_site/src/content/docs/tutorials/security/oauth2-jwt/index.md
+61-0
| @@ -0,0 +1,61 @@ | ||
1 | +--- | |
2 | +title: OAuth2 with Password Hashing and JWT Tokens | |
3 | +description: Implement production-ready OAuth2 with password hashing and JWT tokens in JustAPI. | |
4 | +keywords: [JustAPI, OAuth2, JWT, password hashing, security, authentication] | |
5 | +--- | |
6 | + | |
7 | +## Production OAuth2 with JWT | |
8 | + | |
9 | +```python | |
10 | +import jwt | |
11 | +from datetime import datetime, timedelta | |
12 | +from justapi import JustAPIApp, Depends, HTTPException | |
13 | +from fastapi.security import OAuth2PasswordBearer | |
14 | +from passlib.context import CryptContext | |
15 | + | |
16 | +SECRET_KEY = "your-secret-key" | |
17 | +ALGORITHM = "HS256" | |
18 | +ACCESS_TOKEN_EXPIRE_MINUTES = 30 | |
19 | + | |
20 | +app = JustAPIApp() | |
21 | +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") | |
22 | +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") | |
23 | + | |
24 | +def verify_password(plain: str, hashed: str) -> bool: | |
25 | + return pwd_context.verify(plain, hashed) | |
26 | + | |
27 | +def hash_password(password: str) -> str: | |
28 | + return pwd_context.hash(password) | |
29 | + | |
30 | +def create_access_token(data: dict) -> str: | |
31 | + to_encode = data.copy() | |
32 | + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) | |
33 | + to_encode.update({"exp": expire}) | |
34 | + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) | |
35 | + | |
36 | +def get_current_user(token: str = Security(oauth2_scheme)): | |
37 | + try: | |
38 | + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) | |
39 | + user_id = payload.get("user_id") | |
40 | + if user_id is None: | |
41 | + raise HTTPException(status_code=401) | |
42 | + return {"user_id": user_id} | |
43 | + except jwt.PyJWTError: | |
44 | + raise HTTPException(status_code=401, detail="Invalid token") | |
45 | + | |
46 | +@app.post("/token") | |
47 | +def login(username: str, password: str): | |
48 | + # Verify credentials and return JWT | |
49 | + token = create_access_token({"user_id": 1, "sub": username}) | |
50 | + return {"access_token": token, "token_type": "bearer"} | |
51 | + | |
52 | +@app.get("/users/me") | |
53 | +def read_me(current_user: dict = Security(get_current_user)): | |
54 | + return current_user | |
55 | +``` | |
56 | + | |
57 | +## See Also | |
58 | + | |
59 | +- [Security — First Steps](/tutorials/security/first-steps/) — basic setup | |
60 | +- [Get Current User](/tutorials/security/get-current-user/) — extracting user | |
61 | +- [Simple OAuth2](/tutorials/security/simple-oauth2/) — simpler setup | |
0 | 62 | |
▼docs_site/src/content/docs/tutorials/security/simple-oauth2/index.md
+40-0
| @@ -0,0 +1,40 @@ | ||
1 | +--- | |
2 | +title: Simple OAuth2 with Password and Bearer | |
3 | +description: Implement password-based OAuth2 authentication in JustAPI. | |
4 | +keywords: [JustAPI, OAuth2, password, bearer, authentication, security] | |
5 | +--- | |
6 | + | |
7 | +## Basic OAuth2 Setup | |
8 | + | |
9 | +```python | |
10 | +from justapi import JustAPIApp, Depends, HTTPException | |
11 | +from fastapi.security import OAuth2PasswordBearer | |
12 | + | |
13 | +app = JustAPIApp() | |
14 | + | |
15 | +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") | |
16 | + | |
17 | +# Fake user database | |
18 | +fake_users_db = { | |
19 | + "alice": {"user_id": 1, "name": "Alice", "password": "secret"}, | |
20 | +} | |
21 | + | |
22 | +def authenticate_user(username: str, password: str): | |
23 | + user = fake_users_db.get(username) | |
24 | + if user and user["password"] == password: | |
25 | + return user | |
26 | + return None | |
27 | + | |
28 | +@app.post("/token") | |
29 | +def login(username: str, password: str): | |
30 | + user = authenticate_user(username, password) | |
31 | + if not user: | |
32 | + raise HTTPException(status_code=401, detail="Invalid credentials") | |
33 | + return {"access_token": "fake-jwt-token", "token_type": "bearer"} | |
34 | +``` | |
35 | + | |
36 | +## See Also | |
37 | + | |
38 | +- [Security — First Steps](/tutorials/security/first-steps/) — basic setup | |
39 | +- [Get Current User](/tutorials/security/get-current-user/) — extracting user | |
40 | +- [OAuth2 + JWT](/tutorials/security/oauth2-jwt/) — production JWT | |
0 | 41 | |
▼docs_site/src/content/docs/tutorials/static-files.md
+55-0
| @@ -0,0 +1,55 @@ | ||
1 | +--- | |
2 | +title: Static Files | |
3 | +description: Serve static files (HTML, CSS, JS, images) from a JustAPI application. | |
4 | +keywords: [JustAPI, static files, assets, mount, CSS, JavaScript] | |
5 | +--- | |
6 | + | |
7 | +## Basic Static Files | |
8 | + | |
9 | +Mount a directory to serve static files: | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +app.mount("/static", directory="static", name="static") | |
17 | +``` | |
18 | + | |
19 | +This serves files from the `static/` directory at `/static/`. For example, `static/style.css` is accessible at `/static/style.css`. | |
20 | + | |
21 | +## Custom Mount Point | |
22 | + | |
23 | +```python | |
24 | +# Serve at /assets/* | |
25 | +app.mount("/assets", directory="public/assets", name="assets") | |
26 | +``` | |
27 | + | |
28 | +## Multiple Static Directories | |
29 | + | |
30 | +```python | |
31 | +app.mount("/css", directory="static/css", name="css") | |
32 | +app.mount("/js", directory="static/js", name="js") | |
33 | +app.mount("/images", directory="static/images", name="images") | |
34 | +``` | |
35 | + | |
36 | +## Mounting a Sub-application | |
37 | + | |
38 | +JustAPI supports mounting sub-applications (like Starlette or ASGI apps) at a path: | |
39 | + | |
40 | +```python | |
41 | +from justapi import JustAPIApp | |
42 | + | |
43 | +app = JustAPIApp() | |
44 | + | |
45 | +# Mount a sub-application | |
46 | +from starlette.applications import Starlette | |
47 | +sub_app = Starlette() | |
48 | +app.mount("/api/v2", sub_app, name="v2") | |
49 | +``` | |
50 | + | |
51 | +## See Also | |
52 | + | |
53 | +- [Response Classes](/api-reference/responses/) — response types | |
54 | +- [Custom Response Classes](/advanced/custom-response/) — HTML, stream, file responses | |
55 | +- [Deployment — Docker](/deployment/docker/) — containerized static files | |
0 | 56 | |
▼docs_site/src/content/docs/tutorials/testing.md
+94-0
| @@ -0,0 +1,94 @@ | ||
1 | +--- | |
2 | +title: Testing | |
3 | +description: Test JustAPI applications using the built-in JustAPITestClient for unit and integration tests. | |
4 | +keywords: [JustAPI, testing, test client, pytest, assertions] | |
5 | +--- | |
6 | + | |
7 | +## JustAPITestClient | |
8 | + | |
9 | +JustAPI includes `JustAPITestClient` for testing without running a server. It processes requests in-process using the Rust runtime. | |
10 | + | |
11 | +```python | |
12 | +from justapi import JustAPIApp, JustAPITestClient | |
13 | + | |
14 | +app = JustAPIApp() | |
15 | + | |
16 | +@app.get("/hello") | |
17 | +def hello(): | |
18 | + return {"message": "Hello!"} | |
19 | + | |
20 | +client = JustAPITestClient(app) | |
21 | + | |
22 | +def test_hello(): | |
23 | + response = client.get("/hello") | |
24 | + assert response.status == 200 | |
25 | + assert response.json() == {"message": "Hello!"} | |
26 | +``` | |
27 | + | |
28 | +## Testing POST Endpoints | |
29 | + | |
30 | +```python | |
31 | +def test_create_item(): | |
32 | + response = client.post("/items", body={"name": "Widget", "price": 9.99}) | |
33 | + assert response.status == 200 | |
34 | + assert response.json()["name"] == "Widget" | |
35 | +``` | |
36 | + | |
37 | +## Testing Error Responses | |
38 | + | |
39 | +```python | |
40 | +def test_not_found(): | |
41 | + response = client.get("/nonexistent") | |
42 | + assert response.status == 404 | |
43 | + | |
44 | +def test_validation_error(): | |
45 | + response = client.post("/items", body={"name": "x"}) | |
46 | + assert response.status == 422 # validation error | |
47 | +``` | |
48 | + | |
49 | +## Testing with Path Parameters | |
50 | + | |
51 | +```python | |
52 | +@app.get("/users/{user_id}") | |
53 | +def get_user(user_id: int): | |
54 | + return {"user_id": user_id} | |
55 | + | |
56 | +def test_get_user(): | |
57 | + response = client.get("/users/42") | |
58 | + assert response.json() == {"user_id": 42} | |
59 | +``` | |
60 | + | |
61 | +## Testing with Query Parameters | |
62 | + | |
63 | +```python | |
64 | +@app.get("/search") | |
65 | +def search(q: str, limit: int = 10): | |
66 | + return {"q": q, "limit": limit} | |
67 | + | |
68 | +def test_search_with_defaults(): | |
69 | + response = client.get("/search?q=foo") | |
70 | + assert response.json() == {"q": "foo", "limit": 10} | |
71 | + | |
72 | +def test_search_with_custom_limit(): | |
73 | + response = client.get("/search?q=foo&limit=5") | |
74 | + assert response.json() == {"q": "foo", "limit": 5} | |
75 | +``` | |
76 | + | |
77 | +## Running Tests | |
78 | + | |
79 | +```bash | |
80 | +# Run with pytest | |
81 | +pytest tests/ -v | |
82 | + | |
83 | +# Run with uv | |
84 | +uv run pytest tests/ -v | |
85 | + | |
86 | +# Single test file | |
87 | +pytest tests/test_app.py -v | |
88 | +``` | |
89 | + | |
90 | +## See Also | |
91 | + | |
92 | +- [Testing Client API](/api-reference/testing-client/) — full client reference | |
93 | +- [Debugging](/tutorials/debugging/) — debugging techniques | |
94 | +- [Testing Guide](/contributing/testing-guide/) — writing and running tests | |
0 | 95 | |