Showing preview only (478K chars total). Download the full file or copy to clipboard to get everything.
Repository: realworld-apps/realworld
Branch: main
Commit: a86e4d72186c
Files: 245
Total size: 417.4 KB
Directory structure:
gitextract_60zq8mk5/
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── BUG_REPORT.yml
│ │ └── FEATURE_REQUEST.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── bruno-check.yml
│ ├── codeql.yml
│ ├── deploy-docs.yml
│ └── spammy-guardian.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── assets/
│ ├── media/
│ │ ├── conduit-logo.svg.generate.ts
│ │ └── mobile_icons/
│ │ ├── ios/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── README.md
│ │ └── watchkit/
│ │ └── AppIcon.appiconset/
│ │ └── Contents.json
│ └── theme/
│ └── styles.css
├── docs/
│ ├── .gitignore
│ ├── .vscode/
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── README.md
│ ├── astro.config.mjs
│ ├── non-included/
│ │ └── LICENSES_LOGOS.md
│ ├── package.json
│ ├── src/
│ │ ├── content/
│ │ │ ├── config.ts
│ │ │ └── docs/
│ │ │ ├── community/
│ │ │ │ ├── authors.md
│ │ │ │ ├── resources.md
│ │ │ │ └── special-thanks.md
│ │ │ ├── implementation-creation/
│ │ │ │ ├── expectations.md
│ │ │ │ ├── features.md
│ │ │ │ └── introduction.md
│ │ │ ├── index.mdx
│ │ │ ├── introduction.mdx
│ │ │ └── specifications/
│ │ │ ├── backend/
│ │ │ │ ├── api-response-format.md
│ │ │ │ ├── bruno.md
│ │ │ │ ├── cors.md
│ │ │ │ ├── endpoints.md
│ │ │ │ ├── error-handling.md
│ │ │ │ ├── hurl.md
│ │ │ │ ├── introduction.md
│ │ │ │ ├── postman.md
│ │ │ │ └── tests.md
│ │ │ ├── frontend/
│ │ │ │ ├── api.md
│ │ │ │ ├── routing.md
│ │ │ │ ├── styles.md
│ │ │ │ ├── templates.md
│ │ │ │ └── tests.md
│ │ │ └── mobile-specs/
│ │ │ └── introduction.md
│ │ ├── env.d.ts
│ │ └── tailwind.css
│ └── tsconfig.json
└── specs/
├── api/
│ ├── README.md
│ ├── bruno/
│ │ ├── articles/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-create-article-with-tags.bru
│ │ │ ├── 03-list-all-articles.bru
│ │ │ ├── 04-list-by-author.bru
│ │ │ ├── 05-list-all-articles-with-auth.bru
│ │ │ ├── 06-list-by-author-with-auth.bru
│ │ │ ├── 07-list-by-tag.bru
│ │ │ ├── 08-list-articles-without-auth.bru
│ │ │ ├── 09-get-single-article.bru
│ │ │ ├── 10-update-article-body.bru
│ │ │ ├── 11-verify-update-persisted.bru
│ │ │ ├── 12-update-article-without-taglist-tags-should-be-preserved.bru
│ │ │ ├── 13-update-article-remove-all-tags-with-empty-array.bru
│ │ │ ├── 14-verify-tags-were-actually-removed.bru
│ │ │ ├── 15-update-article-taglist-null-should-be-rejected.bru
│ │ │ ├── 16-delete-article.bru
│ │ │ └── 17-verify-deletion.bru
│ │ ├── auth/
│ │ │ ├── 01-register.bru
│ │ │ ├── 02-login.bru
│ │ │ ├── 03-get-current-user.bru
│ │ │ ├── 04-update-user.bru
│ │ │ ├── 05-verify-update-persisted.bru
│ │ │ ├── 06-update-user-bio-to-empty-string-should-normalize-to-null.bru
│ │ │ ├── 07-verify-empty-string-normalization-persisted.bru
│ │ │ ├── 08-restore-bio-then-set-to-null.bru
│ │ │ ├── 09-update-user-bio-to-null-should-accept-for-nullable-field.bru
│ │ │ ├── 10-verify-null-bio-persisted.bru
│ │ │ ├── 11-restore-bio.bru
│ │ │ ├── 12-update-user-image.bru
│ │ │ ├── 13-verify-image-update-persisted.bru
│ │ │ ├── 14-update-image-to-empty-string-should-normalize-to-null.bru
│ │ │ ├── 15-verify-image-empty-string-normalization-persisted.bru
│ │ │ ├── 16-set-image-then-update-to-null-should-accept-for-nullable-field.bru
│ │ │ ├── 17-put-user.bru
│ │ │ ├── 18-verify-null-image-persisted.bru
│ │ │ ├── 19-update-username-and-email.bru
│ │ │ └── 20-verify-username-email-update-persisted.bru
│ │ ├── bruno.json
│ │ ├── collection.bru
│ │ ├── comments/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-setup-create-article.bru
│ │ │ ├── 03-create-comment.bru
│ │ │ ├── 04-list-comments.bru
│ │ │ ├── 05-list-comments-without-auth.bru
│ │ │ ├── 06-delete-comment.bru
│ │ │ ├── 07-verify-deletion.bru
│ │ │ ├── 08-selective-deletion-create-two-comments-delete-one-verify-the-other-remains.bru
│ │ │ ├── 09-post-comments.bru
│ │ │ ├── 10-verify-two-comments-exist.bru
│ │ │ ├── 11-delete-the-first-comment.bru
│ │ │ ├── 12-verify-only-the-second-comment-remains.bru
│ │ │ └── 13-cleanup.bru
│ │ ├── environments/
│ │ │ └── local.bru
│ │ ├── errors-articles/
│ │ │ ├── 01-create-article-no-auth.bru
│ │ │ ├── 02-get-unknown-slug.bru
│ │ │ ├── 03-update-no-auth.bru
│ │ │ ├── 04-delete-no-auth.bru
│ │ │ ├── 05-get-feed-no-auth.bru
│ │ │ ├── 06-favorite-no-auth.bru
│ │ │ ├── 07-unfavorite-no-auth.bru
│ │ │ ├── 08-setup-register-for-authenticated-error-tests.bru
│ │ │ ├── 09-create-article-empty-title.bru
│ │ │ ├── 10-create-article-empty-description.bru
│ │ │ ├── 11-create-article-empty-body.bru
│ │ │ ├── 12-duplicate-titles-are-allowed-each-gets-a-unique-slug.bru
│ │ │ ├── 13-post-articles.bru
│ │ │ ├── 14-update-unknown-slug.bru
│ │ │ ├── 15-favorite-unknown-slug.bru
│ │ │ ├── 16-unfavorite-unknown-slug.bru
│ │ │ ├── 17-update-unknown-slug.bru
│ │ │ ├── 18-delete-unknown-slug.bru
│ │ │ ├── 19-cleanup.bru
│ │ │ └── 20-delete-slug2.bru
│ │ ├── errors-auth/
│ │ │ ├── 01-register-empty-username.bru
│ │ │ ├── 02-register-empty-email.bru
│ │ │ ├── 03-register-empty-password.bru
│ │ │ ├── 04-register-valid-user-for-duplicate-and-login-tests.bru
│ │ │ ├── 05-register-duplicate-username.bru
│ │ │ ├── 06-register-duplicate-email.bru
│ │ │ ├── 07-login-empty-email.bru
│ │ │ ├── 08-login-empty-password.bru
│ │ │ ├── 09-login-wrong-password.bru
│ │ │ ├── 10-get-user-no-auth.bru
│ │ │ ├── 11-put-user-no-auth.bru
│ │ │ ├── 12-update-email-to-empty-string-should-reject.bru
│ │ │ ├── 13-update-username-to-empty-string-should-reject.bru
│ │ │ ├── 14-update-email-to-null-should-reject.bru
│ │ │ └── 15-update-username-to-null-should-reject.bru
│ │ ├── errors-authorization/
│ │ │ ├── 01-register-user-a.bru
│ │ │ ├── 02-register-user-b.bru
│ │ │ ├── 03-user-a-creates-article.bru
│ │ │ ├── 04-user-b-tries-to-delete-403.bru
│ │ │ ├── 05-user-b-tries-to-update-403.bru
│ │ │ ├── 06-user-a-creates-a-comment-on-the-article.bru
│ │ │ ├── 07-user-b-tries-to-delete-a-s-comment-403.bru
│ │ │ ├── 08-verify-comment-survived-the-failed-delete.bru
│ │ │ └── 09-cleanup-user-a-deletes-article.bru
│ │ ├── errors-comments/
│ │ │ ├── 01-post-comment-no-auth.bru
│ │ │ ├── 02-delete-comment-no-auth.bru
│ │ │ ├── 03-setup-register-create-article.bru
│ │ │ ├── 04-post-articles.bru
│ │ │ ├── 05-post-comment-empty-body.bru
│ │ │ ├── 06-post-comment-on-unknown-article.bru
│ │ │ ├── 07-get-comments-on-unknown-article.bru
│ │ │ ├── 08-delete-comment-on-unknown-article.bru
│ │ │ ├── 09-delete-non-existent-comment-on-existing-article.bru
│ │ │ └── 10-cleanup.bru
│ │ ├── errors-profiles/
│ │ │ ├── 01-get-unknown-profile.bru
│ │ │ ├── 02-follow-no-auth.bru
│ │ │ ├── 03-unfollow-no-auth.bru
│ │ │ ├── 04-setup-register-for-authenticated-404-tests.bru
│ │ │ ├── 05-follow-unknown-user-authed.bru
│ │ │ └── 06-unfollow-unknown-user-authed.bru
│ │ ├── favorites/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-setup-create-article.bru
│ │ │ ├── 03-favorite-article.bru
│ │ │ ├── 04-verify-favorite-persists.bru
│ │ │ ├── 05-articles-filtered-by-favorited-username.bru
│ │ │ ├── 06-articles-filtered-by-favorited-username-with-auth.bru
│ │ │ ├── 07-unfavorite-article.bru
│ │ │ ├── 08-verify-unfavorite-persists.bru
│ │ │ └── 09-cleanup.bru
│ │ ├── feed/
│ │ │ ├── 01-register-main-user.bru
│ │ │ ├── 02-register-celeb-user.bru
│ │ │ ├── 03-feed-for-new-user-returns-empty.bru
│ │ │ ├── 04-main-follows-celeb.bru
│ │ │ ├── 05-celeb-creates-article-1.bru
│ │ │ ├── 06-celeb-creates-article-2.bru
│ │ │ ├── 07-main-checks-feed.bru
│ │ │ ├── 08-feed-with-limit-1.bru
│ │ │ ├── 09-feed-with-limit-1-offset-1.bru
│ │ │ ├── 10-cleanup-delete-articles.bru
│ │ │ ├── 11-delete-slug2.bru
│ │ │ └── 12-cleanup-unfollow.bru
│ │ ├── pagination/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-create-article-1.bru
│ │ │ ├── 03-create-article-2.bru
│ │ │ ├── 04-list-with-limit-1-most-recent-first-so-slug2.bru
│ │ │ ├── 05-list-with-limit-1-offset-1-second-page-so-slug1.bru
│ │ │ ├── 06-cleanup.bru
│ │ │ └── 07-delete-slug2.bru
│ │ ├── profiles/
│ │ │ ├── 01-register-main-user.bru
│ │ │ ├── 02-register-celeb-user.bru
│ │ │ ├── 03-get-profile-without-auth.bru
│ │ │ ├── 04-get-profile-with-auth.bru
│ │ │ ├── 05-follow-profile.bru
│ │ │ ├── 06-unfollow-profile.bru
│ │ │ └── 07-verify-unfollow-persisted.bru
│ │ └── tags/
│ │ ├── 01-setup-register.bru
│ │ ├── 02-setup-create-article-with-tags.bru
│ │ ├── 03-get-tags.bru
│ │ └── 04-cleanup.bru
│ ├── hurl/
│ │ ├── articles.hurl
│ │ ├── auth.hurl
│ │ ├── comments.hurl
│ │ ├── errors_articles.hurl
│ │ ├── errors_auth.hurl
│ │ ├── errors_authorization.hurl
│ │ ├── errors_comments.hurl
│ │ ├── errors_profiles.hurl
│ │ ├── favorites.hurl
│ │ ├── feed.hurl
│ │ ├── pagination.hurl
│ │ ├── profiles.hurl
│ │ ├── run-hurl-tests.sh
│ │ └── tags.hurl
│ ├── hurl-to-bruno.js
│ ├── openapi.yml
│ ├── run-api-tests-bruno.sh
│ └── run-api-tests-hurl.sh
└── e2e/
├── SELECTORS.md
├── articles.spec.ts
├── auth.spec.ts
├── comments.spec.ts
├── error-handling.spec.ts
├── health.spec.ts
├── helpers/
│ ├── api.ts
│ ├── articles.ts
│ ├── auth.ts
│ ├── comments.ts
│ ├── config.ts
│ ├── debug.ts
│ ├── profile.ts
│ └── setup.ts
├── navigation.spec.ts
├── null-fields.spec.ts
├── playwright.base.ts
├── settings.spec.ts
├── social.spec.ts
├── url-navigation.spec.ts
├── user-fetch-errors.spec.ts
└── xss-security.spec.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CODEOWNERS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.yml
================================================
name: 🐞 Bug report
description: Report a bug in the RealWorld project
title: '[Bug]: '
labels:
- bug
body:
- type: dropdown
attributes:
label: Relevant scope
description: What is the scope of this request?
options:
- Frontend specs
- Backend specs
- Deployed demo
- 'Other: describe below'
validations:
required: true
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
validations:
required: true
- type: markdown
attributes:
value: >-
This template was generated with [Issue Forms
Creator](https://www.issue-forms-creator.app/)
================================================
FILE: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
================================================
name: 🚀 Feature request
description: Suggest a feature for RealWorld project
title: '[Feature Request]:'
body:
- type: markdown
attributes:
value: '# Feature Request'
- type: dropdown
attributes:
label: Relevant Scope
description: What is the scope of this request?
options:
- Frontend specs
- Backend specs
- 'Other: describe below'
validations:
required: true
- type: textarea
attributes:
label: Description
description: ' <!-- ✍️--> '
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: If you have a solution in mind, please describe it.
- type: textarea
attributes:
label: Describe alternatives you've considered
description: Have you considered any alternative solutions or workarounds?
- type: markdown
attributes:
value: >-
This template was generated with [Issue Forms
Creator](https://www.issue-forms-creator.app/)
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: npm
directory: '/'
schedule:
interval: weekly
open-pull-requests-limit: 10
================================================
FILE: .github/workflows/bruno-check.yml
================================================
name: 'Bruno Check'
on:
push:
pull_request:
jobs:
bruno-check:
name: Verify Bruno collection is up-to-date
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Check Bruno collection is up-to-date
run: make bruno-check
================================================
FILE: .github/workflows/codeql.yml
================================================
name: 'CodeQL'
on:
workflow_dispatch:
schedule:
- cron: '24 3 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: ['javascript']
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
with:
category: '/language:${{matrix.language}}'
================================================
FILE: .github/workflows/deploy-docs.yml
================================================
name: Deploy Documentation
on:
push:
branches: [main]
paths:
- 'docs/**'
- '.github/workflows/deploy-docs.yml'
workflow_dispatch: # allow manual trigger
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Install dependencies
run: bun install
working-directory: ./docs
- name: Build documentation
run: bun run build
working-directory: ./docs
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
with:
path: './docs/dist'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/spammy-guardian.yml
================================================
name: Spammy Guardian
on:
workflow_dispatch:
inputs:
issueId:
description: 'id of the issue to test againt'
required: true
issue_comment:
issues:
types: [opened]
jobs:
spammy-guardian:
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' || github.actor != 'netlify[bot]' }}
steps:
- uses: kerhub/spammy-guardian@fa79bcda24df6dae5b93285e1749e59c77add4bd
with:
token: ${{ secrets.GITHUB_TOKEN }}
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Dependencies
node_modules
.pnp
.pnp.js
# Local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Testing
coverage
# Turbo
.turbo
# Vercel
.vercel
# Build Outputs
.next/
.nitro/
out/
build
dist
# Debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Misc
.DS_Store
*.pem
# IDEs
.idea
# Python
__pycache__
# Temp files
.tmp
# Local db
dev.db
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to RealWorld
We would love for you to contribute to RealWorld and help make it even better than it is
today! As a contributor, here are the guidelines we would like you to follow:
- [Question or Problem?](#question)
- [Issues and Bugs](#issue)
- [Feature Requests](#feature)
- [Submission Guidelines](#submit)
- [Coding Rules](#rules)
- [Commit Message Guidelines](#commit)
## <a name="question"></a> Got a Question or Problem?
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests.
For open discussions, we encourage you to use the [Github Discussions][github-discussions] channels.
## <a name="issue"></a> Interested in creating Conduit for your framework?
To create an official implementation of Conduit, check out our [Github Discussions](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations) and see if anyone else has requested and/or is already working on your framework.
If not, feel free to start working on one!
Start [here][github-spec]!
## <a name="issue"></a> Found a Bug?
If you find a bug in the project, you can help us by
[submitting an issue][github-issue] to our [GitHub Repository][github]. Even better, you can
[submit a Pull Request](#submit-pr) with a fix.
## <a name="feature"></a> Missing a Feature?
You can _request_ a new feature by [submitting an issue](#submit-issue) to our GitHub
repository.
If you would like to _implement_ a new feature, please submit an issue with
a proposal for your work **FIRST**, to be sure that we can use it.
Please consider what kind of change it is:
- For a **Major Feature**, first open an issue and outline your proposal so that it can be
discussed. This will also allow us to better coordinate our efforts, prevent duplication of work,
and help you to craft the change so that it is successfully accepted into the project.
- **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr).
## <a name="submit"></a> Submission Guidelines
### <a name="submit-issue"></a> Submitting an Issue
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
You can file new issues by selecting from our [new issue templates][github-choose] and filling out the issue template.
### <a name="submit-pr"></a> Submitting a Pull Request (PR)
Before you submit your Pull Request (PR) consider the following guidelines:
1. Search [GitHub](https://github.com/realworld-apps/realworld/pulls) for an open or closed PR
that relates to your submission. You don't want to duplicate effort.
1. Be sure that an issue describes the problem you're fixing, or documents the design for the feature you'd like to add.
Discussing the design up front helps to ensure that we're ready to accept your work.
1. Fork the realworld-apps/realworld repo.
1. Make your changes in a new git branch:
```bash
git checkout -b my-fix-branch master
```
1. Create your patch.
1. Commit your changes using a descriptive commit message that follows our
[commit message conventions](#commit).
1. Push your branch to GitHub:
```bash
git push origin my-fix-branch
```
1. In GitHub, send a pull request to `realworld:master`.
- If we suggest changes then:
- Make the required updates.
- Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
```bash
git rebase master -i
git push -f
```
That's it! Thank you for your contribution!
#### After your pull request is merged
After your pull request is merged, you can safely delete your branch and pull the changes
from the master (upstream) repository:
- Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
```bash
git push origin --delete my-fix-branch
```
- Check out the master branch:
```bash
git checkout master -f
```
- Delete the local branch:
```bash
git branch -D my-fix-branch
```
- Update your master with the latest upstream version:
```bash
git pull --ff upstream master
```
## <a name="commit"></a> Commit Message Guidelines
> These guidelines have been added to the project starting from February 2025
We have very precise rules over how our git commit messages can be formatted. This leads to **more
readable messages** that are easy to follow when looking through the **project history**.
### Commit Message Format
Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
format that includes a **type**, a **scope** and a **subject**:
```
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
```
The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
Samples:
```
docs(changelog): update changelog to beta.5
```
```
fix(release): need to depend on latest ng-lib
The version in our package.json gets copied to the one we publish, and users need the latest of these.
```
### Type
Must be one of the following:
- **docs**: Documentation only changes
- **feat**: A new feature
- **fix**: A bug fix
### Scope
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages).
The following is the list of supported scopes:
- **specs**
- **project**
### Subject
The subject contains a succinct description of the change:
- use the imperative, present tense: "change" not "changed" nor "changes"
- don't capitalize the first letter
- no dot (.) at the end
### Body
Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
The body should include the motivation for the change and contrast this with previous behavior.
### Footer
The footer should contain any information about **Breaking Changes** and is also the place to
reference GitHub issues that this commit **Closes**.
**Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
Samples :
```
Close #394
```
```
BREAKING CHANGE:
change login route to /users/login
```
[github]: https://github.com/realworld-apps/realworld
[github-issue]: https://github.com/realworld-apps/realworld/issues/new?assignees=&labels=bug&template=---bug-report.md&title=
[github-feature]: https://github.com/realworld-apps/realworld/issues/new?assignees=&labels=enhancement&template=---feature-request.md&title=
[github-choose]: https://github.com/realworld-apps/realworld/issues/new/choose
[github-discussions]: https://github.com/realworld-apps/realworld/discussions
[github-spec]: https://github.com/realworld-apps/realworld/tree/master/spec
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 Thinkster
Copyright (c) 2026 c4ffein
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
Note: Third-party framework logos in assets/media/frameworks.svg are NOT covered
by this license. See docs/non-included/LICENSES_LOGOS.md for their respective
licenses and attribution.
================================================
FILE: Makefile
================================================
.PHONY: help \
bruno-generate \
bruno-check \
documentation-setup \
documentation-dev \
documentation-dev-host \
documentation-build \
documentation-preview \
documentation-clean
help:
@echo "Bruno Collection:"
@echo " bruno-generate"
@echo " bruno-check"
@echo ""
@echo "Documentation:"
@echo " documentation-setup"
@echo " documentation-dev"
@echo " documentation-dev-host"
@echo " documentation-build"
@echo " documentation-preview"
@echo " documentation-clean"
########################
# Bruno Collection
bruno-generate:
bun specs/api/hurl-to-bruno.js
bruno-check:
bun specs/api/hurl-to-bruno.js --check
########################
# Documentation
documentation-setup:
cd docs && bun install
documentation-dev:
cd docs && bun run dev
documentation-dev-host:
cd docs && bun run dev --host
documentation-build:
cd docs && bun run build
documentation-preview:
cd docs && bun run preview
documentation-clean:
rm -rf docs/.astro docs/dist docs/node_modules
================================================
FILE: README.md
================================================

<p align="center" style="margin-top: 30px;">
<img src="assets/media/frameworks.svg" alt="Frontend and Backend Frameworks" width="720"/>
</p>
### See how [_the exact same_ Medium.com clone](https://demo.realworld.show) is built using different [frontends](https://codebase.show/projects/realworld?category=frontend) and [backends](https://codebase.show/projects/realworld?category=backend)
You can combine any frontend with any backend, because **they all adhere to the same API spec**
While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build _real_ applications with it.
**RealWorld** solves this by allowing you to choose any frontend (React, Angular, & more) and any backend (Node, Django, & more).
_Read the [full blog post announcing RealWorld on Medium.](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5)_
Join us on [GitHub Discussions!](https://github.com/realworld-apps/realworld/discussions) 🎉
# Implementations
Over 100 implementations have been created using various languages, libraries, and frameworks.
Explore them on [**CodebaseShow**](https://codebase.show/projects/realworld).
## Spec-compliant backends
These backends pass the full [API spec test suite](specs/api/):
- [**Nitro + Prisma + Zod**](https://github.com/realworld-apps/nitro-prisma-zod-realworld-example-app) — TypeScript
- [**Django Ninja**](https://github.com/c4ffein/realworld-django-ninja) — Python
# Create a new implementation
[**Create a new implementation >>>**](https://docs.realworld.show/implementation-creation/introduction)
Or you can [view upcoming implementations (WIPs)](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations).
# Learn more
- [Documentation introduction](https://docs.realworld.show/introduction/)
- Every tutorial is built against the same [API spec](specs/api/) to ensure modularity of every frontend & backend
- A shared [CSS theme](assets/theme/styles.css) is provided to build frontend implementations with identical UI/UX
- A shared [E2E test suite](specs/e2e/) is available to validate frontend implementations
- There is a hosted version of the backend API available for public usage at [api.realworld.show](https://api.realworld.show) (with strong account isolation), no API keys are required
- There is an angular frontend plugged to this backend available at [demo.realworld.show](https://demo.realworld.show)
- Interested in creating a new RealWorld stack? View our [starter guide & spec](https://docs.realworld.show/implementation-creation/introduction)
# Logo Attribution
See [LICENSES_LOGOS.md](docs/non-included/LICENSES_LOGOS.md) for framework logo licensing and attribution details.
# Active Maintainers
- **[c4ffein](https://github.com/c4ffein) - Maintainer** - currently maintains the [demo website](https://demo.realworld.show)
- **[Manuel Vila](https://github.com/mvila) - Maintainer** - creator of the [Layr framework](https://layrjs.com) and the [CodebaseShow website](https://codebase.show/)
================================================
FILE: assets/media/conduit-logo.svg.generate.ts
================================================
import opentype from 'opentype.js';
import { mkdirSync } from 'fs';
process.chdir(import.meta.dir);
const FONT_URL = 'https://fonts.gstatic.com/s/caudex/v19/esDT311QOP6BJUrwdteUkp8G.ttf';
const TMP_DIR = '.tmp';
const FONT_PATH = `${TMP_DIR}/caudex-bold.ttf`;
// Ensure .tmp directory exists and download font if needed
mkdirSync(TMP_DIR, { recursive: true });
const fontFile = Bun.file(FONT_PATH);
if (!(await fontFile.exists())) {
console.log('Downloading Caudex Bold...');
const res = await fetch(FONT_URL);
await Bun.write(FONT_PATH, res);
}
const font = opentype.loadSync(FONT_PATH);
const text = 'Conduit';
const fontSize = 48;
const path = font.getPath(text, 0, 0, fontSize);
const bb = path.getBoundingBox();
const padding = 2;
const x = bb.x1 - padding;
const y = bb.y1 - padding;
const width = bb.x2 - bb.x1 + padding * 2;
const height = bb.y2 - bb.y1 + padding * 2;
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${x} ${y} ${width} ${height}" fill="#222">
${path.toSVG(2)}
</svg>`;
const outPath = 'conduit-logo.svg';
await Bun.write(outPath, svg);
console.log(`Written to ${outPath} (${width.toFixed(1)} x ${height.toFixed(1)})`);
================================================
FILE: assets/media/mobile_icons/ios/AppIcon.appiconset/Contents.json
================================================
{
"images": [
{
"idiom": "iphone",
"size": "20x20",
"scale": "2x",
"filename": "Icon-App-20x20@2x.png"
},
{
"idiom": "iphone",
"size": "20x20",
"scale": "3x",
"filename": "Icon-App-20x20@3x.png"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "1x",
"filename": "Icon-App-29x29@1x.png"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "2x",
"filename": "Icon-App-29x29@2x.png"
},
{
"idiom": "iphone",
"size": "29x29",
"scale": "3x",
"filename": "Icon-App-29x29@3x.png"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "1x",
"filename": "Icon-App-40x40@1x.png"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "2x",
"filename": "Icon-App-40x40@2x.png"
},
{
"idiom": "iphone",
"size": "40x40",
"scale": "3x",
"filename": "Icon-App-40x40@3x.png"
},
{
"idiom": "iphone",
"size": "57x57",
"scale": "1x",
"filename": "Icon-App-57x57@1x.png"
},
{
"idiom": "iphone",
"size": "57x57",
"scale": "2x",
"filename": "Icon-App-57x57@2x.png"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "1x",
"filename": "Icon-App-60x60@1x.png"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "2x",
"filename": "Icon-App-60x60@2x.png"
},
{
"idiom": "iphone",
"size": "60x60",
"scale": "3x",
"filename": "Icon-App-60x60@3x.png"
},
{
"idiom": "iphone",
"size": "76x76",
"scale": "1x",
"filename": "Icon-App-76x76@1x.png"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "1x",
"filename": "Icon-App-20x20@1x.png"
},
{
"idiom": "ipad",
"size": "20x20",
"scale": "2x",
"filename": "Icon-App-20x20@2x.png"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "1x",
"filename": "Icon-App-29x29@1x.png"
},
{
"idiom": "ipad",
"size": "29x29",
"scale": "2x",
"filename": "Icon-App-29x29@2x.png"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "1x",
"filename": "Icon-App-40x40@1x.png"
},
{
"idiom": "ipad",
"size": "40x40",
"scale": "2x",
"filename": "Icon-App-40x40@2x.png"
},
{
"size": "50x50",
"idiom": "ipad",
"filename": "Icon-Small-50x50@1x.png",
"scale": "1x"
},
{
"size": "50x50",
"idiom": "ipad",
"filename": "Icon-Small-50x50@2x.png",
"scale": "2x"
},
{
"idiom": "ipad",
"size": "72x72",
"scale": "1x",
"filename": "Icon-App-72x72@1x.png"
},
{
"idiom": "ipad",
"size": "72x72",
"scale": "2x",
"filename": "Icon-App-72x72@2x.png"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "1x",
"filename": "Icon-App-76x76@1x.png"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "2x",
"filename": "Icon-App-76x76@2x.png"
},
{
"idiom": "ipad",
"size": "76x76",
"scale": "3x",
"filename": "Icon-App-76x76@3x.png"
},
{
"idiom": "ipad",
"size": "83.5x83.5",
"scale": "2x",
"filename": "Icon-App-83.5x83.5@2x.png"
}
],
"info": {
"version": 1,
"author": "makeappicon"
}
}
================================================
FILE: assets/media/mobile_icons/ios/README.md
================================================
## iTunesArtwork & iTunesArtwork@2x (App Icon) file extension:
PNG extension is prepended to these two files -
While Apple suggested to omit the extension for these files,
the '.png' extension is actually required for iTunesConnect submission.
This is done for you so you don't have to.
However, for Ad_hoc or Enterprise distribution, the extension should be removed
from the files before adding to XCode to avoid error.
refs: https://developer.apple.com/library/ios/qa/qa1686/_index.html
## iTunesArtwork & iTunesArtwork@2x (App Icon) transparency handling:
As images with alpha channels or transparencies cannot be set as an application's icon on
iTunesConnect, all transparent pixels in your images will be converted into
solid blacks.
To achieve the best result, you're advised to adjust the transparency settings
in your source files before converting them with makeAppIcon.
refs: https://developer.apple.com/library/ios/documentation/UserExperience/Conceptual/MobileHIG/AppIcons.html
================================================
FILE: assets/media/mobile_icons/watchkit/AppIcon.appiconset/Contents.json
================================================
{
"images": [
{
"size": "24x24",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-24@2x.png",
"role": "notificationCenter",
"subtype": "38mm"
},
{
"size": "27.5x27.5",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-27.5@2x.png",
"role": "notificationCenter",
"subtype": "42mm"
},
{
"size": "29x29",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-29@2x.png",
"role": "companionSettings"
},
{
"size": "29x29",
"idiom": "watch",
"scale": "3x",
"filename": "Icon-29@3x.png",
"role": "companionSettings"
},
{
"size": "40x40",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-40@2x.png",
"role": "appLauncher",
"subtype": "38mm"
},
{
"size": "44x44",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-44@2x.png",
"role": "longLook",
"subtype": "42mm"
},
{
"size": "86x86",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-86@2x.png",
"role": "quickLook",
"subtype": "38mm"
},
{
"size": "98x98",
"idiom": "watch",
"scale": "2x",
"filename": "Icon-98@2x.png",
"role": "quickLook",
"subtype": "42mm"
}
],
"info": {
"version": 1,
"author": "makeappicon"
}
}
================================================
FILE: assets/theme/styles.css
================================================
/*
* ============================================================================
* Conduit Minimal CSS v4
* Only includes classes actually used in this codebase
* ============================================================================
*
* USED CLASSES:
* Layout: container, row, col-xs-12, col-md-{3,6,8,9,10,12}, offset-md-{1,2,3}
* Nav: navbar, navbar-{light,brand,nav}, nav, nav-{pills,item,link}, outline-active
* Buttons: btn, btn-{sm,lg,primary,secondary}, btn-outline-{primary,secondary,danger}
* Forms: form-group, form-control, form-control-lg
* Cards: card, card-{block,footer,text}, comment-form
* Pagination: pagination, page-item, page-link
* Tags: tag-list, tag-default, tag-pill, tag-outline
* Pages: home-page, auth-page, settings-page, editor-page, profile-page, article-page
* Article: article-preview, article-meta, article-content, article-actions, articles-toggle
* Comments: comment-author, comment-author-img, mod-options, date-posted
* User: user-info, user-img, user-pic
* Misc: banner, logo-font, sidebar, feed-toggle, error-messages, counter,
* preview-link, author, date, info, attribution, active, disabled,
* pull-xs-right, text-xs-center, empty-feed-message
*
* NOTE: auth-page, settings-page, and empty-feed-message are semantic hooks
* used in templates but require no dedicated styles.
*
* ============================================================================
*/
:root {
--brand: #33aa44;
--brand-hover: #2b8e3a;
--brand-active: #237630;
--brand-dark: #237630;
--brand-light: #7dd88b;
--danger: #b85c5c;
--danger-light: #d7a3a3;
--secondary: #ccc;
--secondary-hover: #b3b3b3;
--secondary-border: #adadad;
--text: #373a3c;
--text-muted: #999;
--text-light: #bbb;
--text-lighter: #aaa;
--text-disabled: #818a91;
--border: #eee;
--bg-light: #f3f3f3;
--input-focus: var(--brand);
--content-width: 720px;
--content-breakpoint: 1080px;
}
/* ============================================================================
* CSS RESET & BASE
* ============================================================================ */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
-webkit-tap-highlight-color: transparent;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
font-family: 'Source Sans Pro', sans-serif;
font-size: 1rem;
line-height: 1.5;
color: var(--text);
background-color: #fff;
}
/* Main content area grows to push footer down */
app-root {
display: flex;
flex-direction: column;
flex: 1;
}
app-root > *:not(app-layout-footer) {
flex-shrink: 0;
}
app-root > app-layout-footer,
app-root > footer {
margin-top: auto;
}
ol,
ul {
list-style: none;
}
hr {
margin-top: 1rem;
margin-bottom: 1rem;
border: 0;
border-top: 1px solid #eee;
}
a {
color: var(--brand);
text-decoration: none;
background-color: transparent;
transition: color 0.15s ease;
}
a:focus,
a:hover {
color: var(--brand-dark);
text-decoration: underline;
}
a:focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
a:not([href]) {
color: inherit;
text-decoration: none;
}
a:not([href]):focus,
a:not([href]):hover {
color: inherit;
text-decoration: none;
}
a:not([href]):focus {
outline: none;
}
img {
vertical-align: middle;
border: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.1;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.75rem;
}
h4 {
font-size: 1.5rem;
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-bottom: 1rem;
}
.logo-font {
font-family: 'Lora', serif;
}
.navbar-logo {
height: 1.25rem;
vertical-align: middle;
}
.banner-logo {
height: 3rem;
}
.footer-logo {
height: 1rem;
display: block;
}
button,
input,
optgroup,
select,
textarea {
font: inherit;
color: inherit;
margin: 0;
}
button {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
html input[type='button'],
input[type='reset'],
input[type='submit'] {
-webkit-appearance: button;
cursor: pointer;
}
button[disabled],
html input[disabled] {
cursor: default;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
input {
line-height: normal;
}
textarea {
overflow: auto;
resize: vertical;
}
input,
button,
select,
textarea {
line-height: inherit;
border-radius: 0;
}
a,
area,
button,
[role='button'],
input,
label,
select,
summary,
textarea {
touch-action: manipulation;
}
button:focus {
outline: 1px dotted;
outline: 5px auto -webkit-focus-ring-color;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
/* ============================================================================
* LAYOUT: CONTAINER & GRID
* ============================================================================ */
.container {
margin-left: auto;
margin-right: auto;
padding-left: 15px;
padding-right: 15px;
}
@media (min-width: 544px) {
.container {
max-width: 576px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 940px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
.row {
display: flex;
flex-wrap: wrap;
margin-left: -15px;
margin-right: -15px;
}
.col-xs-12 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 100%;
max-width: 100%;
}
@media (min-width: 768px) {
.col-md-3 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 25%;
max-width: 25%;
}
.col-md-6 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 50%;
max-width: 50%;
}
.col-md-8 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 66.66667%;
max-width: 66.66667%;
}
.col-md-9 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 75%;
max-width: 75%;
}
.col-md-10 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 83.33333%;
max-width: 83.33333%;
}
.col-md-12 {
position: relative;
min-height: 1px;
padding-right: 15px;
padding-left: 15px;
flex: 0 0 100%;
max-width: 100%;
}
.offset-md-1 {
margin-left: 8.33333%;
}
.offset-md-2 {
margin-left: 16.66667%;
}
.offset-md-3 {
margin-left: 25%;
}
}
/* ============================================================================
* FORMS
* ============================================================================ */
.form-control {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
line-height: 1.25;
color: #55595c;
background-color: #fff;
background-image: none;
background-clip: padding-box;
border: 1px solid #eee;
border-radius: 0.5rem;
transition: border-color 0.15s ease;
}
.form-control:focus {
border-color: var(--input-focus);
outline: none;
}
.form-control::placeholder {
color: var(--text-muted);
opacity: 1;
}
.form-control:disabled,
.form-control[readonly] {
background-color: #eceeef;
opacity: 1;
}
.form-control:disabled {
cursor: not-allowed;
}
.form-control-lg {
padding: 0.75rem 1.5rem;
font-size: 1.25rem;
border-radius: 0.625rem;
}
.form-group {
margin-bottom: 1rem;
}
/* ============================================================================
* BUTTONS
* ============================================================================ */
.btn {
display: inline-block;
font-weight: normal;
line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
user-select: none;
border: 1px solid transparent;
padding: 0.5rem 1rem;
font-size: 1rem;
border-radius: 0.5rem;
transition:
color 0.15s ease,
background-color 0.15s ease,
border-color 0.15s ease,
box-shadow 0.15s ease;
}
.btn:focus,
.btn.focus,
.btn:active:focus,
.btn:active.focus,
.btn.active:focus,
.btn.active.focus {
outline: thin dotted;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
.btn:focus,
.btn:hover {
text-decoration: none;
}
.btn.focus {
text-decoration: none;
}
.btn:active,
.btn.active {
background-image: none;
outline: 0;
}
.btn.disabled,
.btn:disabled {
cursor: not-allowed;
opacity: 0.65;
}
a.btn.disabled,
fieldset[disabled] a.btn {
pointer-events: none;
}
/* btn-primary */
.btn-primary {
color: #fff;
background-color: var(--brand);
border-color: var(--brand);
}
.btn-primary:hover {
color: #fff;
background-color: var(--brand-hover);
border-color: var(--brand-hover);
}
.btn-primary:focus,
.btn-primary.focus {
color: #fff;
background-color: var(--brand-hover);
border-color: var(--brand-hover);
}
.btn-primary:active,
.btn-primary.active {
color: #fff;
background-color: var(--brand-hover);
border-color: var(--brand-hover);
background-image: none;
}
.btn-primary:active:hover,
.btn-primary:active:focus,
.btn-primary:active.focus,
.btn-primary.active:hover,
.btn-primary.active:focus,
.btn-primary.active.focus {
color: #fff;
background-color: var(--brand-active);
border-color: var(--brand-active);
}
.btn-primary.disabled:focus,
.btn-primary.disabled.focus,
.btn-primary:disabled:focus,
.btn-primary:disabled.focus {
background-color: var(--brand);
border-color: var(--brand);
}
.btn-primary.disabled:hover,
.btn-primary:disabled:hover {
background-color: var(--brand);
border-color: var(--brand);
}
/* btn-secondary */
.btn-secondary {
color: #fff;
background-color: var(--secondary);
border-color: var(--secondary);
}
.btn-secondary:hover {
color: #fff;
background-color: var(--secondary-hover);
border-color: var(--secondary-border);
}
.btn-secondary:focus,
.btn-secondary.focus {
color: #fff;
background-color: var(--secondary-hover);
border-color: var(--secondary-border);
}
.btn-secondary:active,
.btn-secondary.active {
color: #fff;
background-color: var(--secondary-hover);
border-color: var(--secondary-border);
background-image: none;
}
.btn-secondary.disabled:focus,
.btn-secondary.disabled.focus,
.btn-secondary:disabled:focus,
.btn-secondary:disabled.focus {
background-color: var(--secondary);
border-color: var(--secondary);
}
.btn-secondary.disabled:hover,
.btn-secondary:disabled:hover {
background-color: var(--secondary);
border-color: var(--secondary);
}
/* btn-outline-primary */
.btn-outline-primary {
color: var(--brand);
background-image: none;
background-color: transparent;
border-color: var(--brand);
}
.btn-outline-primary:hover {
color: #fff;
background-color: var(--brand);
border-color: var(--brand);
}
.btn-outline-primary:focus,
.btn-outline-primary.focus {
color: #fff;
background-color: var(--brand);
border-color: var(--brand);
}
.btn-outline-primary:active,
.btn-outline-primary.active {
color: #fff;
background-color: var(--brand);
border-color: var(--brand);
}
.btn-outline-primary.disabled:focus,
.btn-outline-primary.disabled.focus,
.btn-outline-primary:disabled:focus,
.btn-outline-primary:disabled.focus {
border-color: var(--brand-light);
}
.btn-outline-primary.disabled:hover,
.btn-outline-primary:disabled:hover {
border-color: var(--brand-light);
}
/* btn-outline-secondary */
.btn-outline-secondary {
color: var(--secondary);
background-image: none;
background-color: transparent;
border-color: var(--secondary);
}
.btn-outline-secondary:hover {
color: #fff;
background-color: var(--secondary);
border-color: var(--secondary);
}
.btn-outline-secondary:focus,
.btn-outline-secondary.focus {
color: #fff;
background-color: var(--secondary);
border-color: var(--secondary);
}
.btn-outline-secondary:active,
.btn-outline-secondary.active {
color: #fff;
background-color: var(--secondary);
border-color: var(--secondary);
}
.btn-outline-secondary.disabled:focus,
.btn-outline-secondary.disabled.focus,
.btn-outline-secondary:disabled:focus,
.btn-outline-secondary:disabled.focus {
border-color: white;
}
.btn-outline-secondary.disabled:hover,
.btn-outline-secondary:disabled:hover {
border-color: white;
}
/* btn-outline-danger */
.btn-outline-danger {
color: var(--danger);
background-image: none;
background-color: transparent;
border-color: var(--danger);
}
.btn-outline-danger:hover {
color: #fff;
background-color: var(--danger);
border-color: var(--danger);
}
.btn-outline-danger:focus,
.btn-outline-danger.focus {
color: #fff;
background-color: var(--danger);
border-color: var(--danger);
}
.btn-outline-danger:active,
.btn-outline-danger.active {
color: #fff;
background-color: var(--danger);
border-color: var(--danger);
}
.btn-outline-danger.disabled:focus,
.btn-outline-danger.disabled.focus,
.btn-outline-danger:disabled:focus,
.btn-outline-danger:disabled.focus {
border-color: var(--danger-light);
}
.btn-outline-danger.disabled:hover,
.btn-outline-danger:disabled:hover {
border-color: var(--danger-light);
}
/* Button sizes */
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.25rem;
border-radius: 0.625rem;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
border-radius: 0.375rem;
}
/* ============================================================================
* NAVIGATION
* ============================================================================ */
.nav {
padding-left: 0;
margin-bottom: 0;
list-style: none;
}
.nav-link {
display: inline-block;
transition:
color 0.15s ease,
background-color 0.15s ease;
}
.nav-link:focus,
.nav-link:hover {
text-decoration: none;
}
.nav-link.disabled,
.nav-link.disabled:focus,
.nav-link.disabled:hover {
color: var(--text-disabled);
cursor: not-allowed;
background-color: transparent;
}
.nav-pills {
display: flex;
flex-wrap: wrap;
}
.nav-pills .nav-item + .nav-item {
margin-left: 0.2rem;
}
.nav-pills .nav-link {
display: block;
padding: 0.5em 1em;
border-radius: 0.5rem;
}
.nav-pills .nav-link.active,
.nav-pills .nav-link.active:focus,
.nav-pills .nav-link.active:hover {
color: #fff;
cursor: default;
background-color: var(--brand);
}
/* Navbar */
.navbar {
position: relative;
padding: 0.5rem 1rem;
}
.navbar > .container {
display: flex;
align-items: center;
}
.navbar-brand {
padding-top: 0;
padding-bottom: 0.25rem;
margin-right: 2rem;
font-family: 'Lora', serif;
font-size: 1.5rem;
color: #222;
}
.navbar-brand:focus,
.navbar-brand:hover {
text-decoration: none;
color: #222;
}
.navbar-nav {
display: flex;
align-items: center;
margin-left: auto;
}
.navbar-nav .nav-link {
display: block;
padding-top: 0.425rem;
padding-bottom: 0.425rem;
}
.navbar-nav .nav-link + .nav-link {
margin-left: 1rem;
}
.navbar-nav .nav-item + .nav-item {
margin-left: 1rem;
}
.navbar-light .navbar-brand,
.navbar-light .navbar-brand:focus,
.navbar-light .navbar-brand:hover {
color: #222;
}
.navbar-light .navbar-nav .nav-link {
color: rgba(0, 0, 0, 0.3);
}
.navbar-light .navbar-nav .nav-link:focus,
.navbar-light .navbar-nav .nav-link:hover {
color: rgba(0, 0, 0, 0.6);
}
.navbar-light .navbar-nav .active > .nav-link,
.navbar-light .navbar-nav .active > .nav-link:focus,
.navbar-light .navbar-nav .active > .nav-link:hover,
.navbar-light .navbar-nav .nav-link.active,
.navbar-light .navbar-nav .nav-link.active:focus,
.navbar-light .navbar-nav .nav-link.active:hover {
color: rgba(0, 0, 0, 0.8);
}
.nav-link .user-pic {
height: 26px;
border-radius: 50px;
float: left;
margin-right: 5px;
}
.nav-link:hover {
text-decoration: none;
}
.nav-signup {
color: #fff !important;
background: var(--brand);
border: 1px solid var(--brand);
border-radius: 99em;
padding: 5px 12px !important;
font-size: 0.8125rem;
line-height: 1.25rem;
transition:
background 200ms ease,
border-color 200ms ease;
}
.nav-signup:hover,
.nav-signup:focus {
color: #fff !important;
background: var(--brand-hover);
border-color: var(--brand-hover);
}
.nav-pills.outline-active .nav-link {
border-radius: 0;
border: none;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--text-lighter);
}
.nav-pills.outline-active .nav-link:hover {
color: #555;
}
.nav-pills.outline-active .nav-link.active {
background: #fff;
border-bottom: 2px solid var(--brand);
color: var(--brand);
}
/* ============================================================================
* CARDS
* ============================================================================ */
.card {
position: relative;
display: block;
margin-bottom: 0.75rem;
background-color: #fff;
border-radius: 0.75rem;
border: 1px solid #eee;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.08),
0 1px 2px rgba(0, 0, 0, 0.06);
transition: box-shadow 0.2s ease;
}
.card-block {
padding: 1.25rem;
}
.card-block::after {
content: '';
display: table;
clear: both;
}
.card-text:last-child {
margin-bottom: 0;
}
.card-footer {
padding: 0.75rem 1.25rem;
background-color: #f5f5f5;
border-top: 1px solid #eee;
}
.card-footer::after {
content: '';
display: table;
clear: both;
}
.card-footer:last-child {
border-radius: 0 0 0.75rem 0.75rem;
}
/* ============================================================================
* PAGINATION
* ============================================================================ */
.pagination {
display: inline-block;
padding-left: 0;
margin-top: 1rem;
margin-bottom: 1rem;
border-radius: 0.5rem;
}
.page-item {
display: inline;
}
.page-item:first-child .page-link {
margin-left: 0;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem;
}
.page-item:last-child .page-link {
border-bottom-right-radius: 0.5rem;
border-top-right-radius: 0.5rem;
}
.page-item.active .page-link,
.page-item.active .page-link:focus,
.page-item.active .page-link:hover {
z-index: 2;
color: #222;
cursor: default;
background-color: #eee;
border-color: #eee;
}
.page-item.disabled .page-link,
.page-item.disabled .page-link:focus,
.page-item.disabled .page-link:hover {
color: var(--text-disabled);
pointer-events: none;
cursor: not-allowed;
background-color: #fff;
border-color: var(--border);
}
.page-link {
position: relative;
float: left;
padding: 0.5rem 0.75rem;
margin-left: -1px;
color: var(--brand);
text-decoration: none;
background-color: #fff;
border: 1px solid var(--border);
transition:
color 0.15s ease,
background-color 0.15s ease;
}
.page-link:focus,
.page-link:hover {
color: var(--brand-dark);
background-color: #eceeef;
border-color: var(--border);
}
/* ============================================================================
* TAGS
* ============================================================================ */
.tag-pill {
padding-right: 0.6em;
padding-left: 0.6em;
border-radius: 10rem;
}
.tag-default {
background-color: transparent;
color: #222;
border: 1px solid #eee;
border-radius: 100px;
font-size: 0.8rem;
line-height: 1.1rem;
padding: 0.35rem 0.75rem;
white-space: nowrap;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
display: inline-block;
transition: background 300ms ease;
}
.tag-default:hover {
text-decoration: none;
background-color: #eee;
}
.tag-default[href]:focus,
.tag-default[href]:hover {
background-color: #eee;
}
.tag-default.tag-outline {
border: 1px solid var(--border);
color: var(--text-lighter);
background: none;
}
ul.tag-list {
display: inline-block;
}
ul.tag-list li {
display: inline-block;
}
/* ============================================================================
* UTILITIES
* ============================================================================ */
.pull-xs-right {
float: right !important;
}
.text-xs-center {
text-align: center !important;
}
/* ============================================================================
* FOOTER
* ============================================================================ */
footer {
background: #fff;
border-top: 1px solid #eee;
margin-top: 42px;
padding: 1rem 0;
width: 100%;
}
footer .logo-font {
color: #222;
}
footer a {
color: #222;
}
footer .attribution {
margin-left: 10px;
font-size: 0.8rem;
color: var(--text-light);
font-weight: 300;
}
/* ============================================================================
* ERROR MESSAGES
* ============================================================================ */
.error-messages {
color: var(--danger);
font-weight: bold;
padding-left: 2.5rem;
margin-bottom: 1rem;
list-style: disc;
}
/* ============================================================================
* BANNER
* ============================================================================ */
.banner {
color: var(--text);
background: #fff;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
padding: 2rem;
margin-bottom: 2rem;
}
.banner h1 {
margin-bottom: 0;
}
.container.page {
margin-top: 1.5rem;
}
/* ============================================================================
* ARTICLE PREVIEW & META
* ============================================================================ */
.preview-link {
color: inherit;
}
.preview-link:hover {
text-decoration: inherit;
}
.article-meta {
display: block;
position: relative;
font-weight: 300;
}
.article-meta img {
display: inline-block;
vertical-align: middle;
height: 32px;
width: 32px;
border-radius: 30px;
}
.article-meta .btn + .btn,
.article-meta app-follow-button + app-favorite-button,
.article-meta app-favorite-button {
margin-left: 0.5rem;
}
.article-meta .info {
margin: 0 1.5rem 0 0.5rem;
display: inline-block;
vertical-align: middle;
line-height: 1rem;
}
.article-meta .info .author {
display: block;
font-weight: 500;
}
.article-meta .info .date {
color: var(--text-light);
font-size: 0.8rem;
display: block;
}
.article-preview {
border-top: 1px solid #eee;
padding: 1.5rem 0;
transition: background-color 0.2s ease;
}
.article-preview:hover {
background-color: rgba(0, 0, 0, 0.01);
}
.article-preview .article-meta {
margin: 0 0 1rem 0;
}
.article-preview .preview-link h1 {
font-weight: 600;
font-size: 1.5rem;
line-height: 1.1;
margin-bottom: 3px;
}
.article-preview .preview-link p {
font-weight: 300;
font-size: 1rem;
color: var(--text-muted);
margin-bottom: 15px;
line-height: 1.3rem;
}
.article-preview .preview-link span {
max-width: 30%;
font-size: 0.8rem;
font-weight: 300;
color: var(--text-light);
vertical-align: middle;
}
.article-preview .preview-link ul {
float: right;
max-width: 50%;
vertical-align: top;
}
.article-preview .preview-link ul li {
font-weight: 300;
font-size: 0.8rem;
padding-top: 0;
padding-bottom: 0;
}
.btn .counter {
font-size: 0.8rem;
}
/* ============================================================================
* HOME PAGE
* ============================================================================ */
.home-page .banner p {
text-align: center;
font-size: 1rem;
font-weight: 300;
color: var(--text-muted);
margin-bottom: 0;
}
.home-page .banner p a {
color: var(--brand);
text-decoration: underline;
}
.home-page .banner h1 {
font-weight: 700;
text-align: center;
font-size: 3.5rem;
line-height: 1.1;
padding-bottom: 0.5rem;
}
.home-page .feed-toggle {
margin-bottom: -1px;
}
.home-page .sidebar {
margin-left: 10px;
padding-left: 20px;
background: none;
border-left: 1px solid #eee;
}
.home-page .sidebar p {
margin-bottom: 0.2rem;
}
/* ============================================================================
* ARTICLE PAGE
* ============================================================================ */
.article-page .banner {
padding: 2rem 0;
}
.article-page .banner h1 {
font-size: 2.8rem;
font-weight: 600;
color: #222;
}
.article-page .banner .btn {
opacity: 0.8;
}
.article-page .banner .btn:hover {
transition: 0.1s all;
opacity: 1;
}
.article-page .banner .article-meta {
margin: 2rem 0 0 0;
}
.article-page .banner .article-meta .author {
color: var(--text);
}
.article-page .banner > .container,
.article-page .article-content,
.article-page .article-actions,
.article-page .container.page > .row {
max-width: var(--content-width);
margin-left: auto;
margin-right: auto;
}
.article-page .article-content.row,
.article-page .container.page > .row {
margin-left: auto;
margin-right: auto;
}
.article-page .article-content .col-md-12,
.article-page .container.page > .row > [class*='col-'] {
padding-left: 0;
padding-right: 0;
flex: 0 0 100%;
max-width: 100%;
margin-left: 0;
}
footer > .container {
display: flex;
align-items: center;
max-width: var(--content-width);
margin-left: auto;
margin-right: auto;
}
.article-page .article-content p {
font-family: 'Lora', serif;
font-size: 1.2rem;
line-height: 1.8rem;
margin-bottom: 2rem;
}
.article-page .article-content h1,
.article-page .article-content h2,
.article-page .article-content h3,
.article-page .article-content h4,
.article-page .article-content h5,
.article-page .article-content h6 {
font-weight: 500;
margin: 1.6rem 0 1rem 0;
}
.article-page .article-content ul,
.article-page .article-content ol {
margin-bottom: 1rem;
padding-left: 2.5rem;
}
.article-page .article-content ul ul,
.article-page .article-content ul ol,
.article-page .article-content ol ul,
.article-page .article-content ol ol {
margin-bottom: 0;
padding-left: 2.5rem;
}
.article-page .article-content ul {
list-style: disc;
}
.article-page .article-content ul ul {
list-style: circle;
}
.article-page .article-content ul ul ul {
list-style: square;
}
.article-page .article-content ol {
list-style: decimal;
}
.article-page .article-content ol ol {
list-style: lower-alpha;
}
.article-page .article-content ol ol ol {
list-style: lower-roman;
}
.article-page .article-actions {
text-align: center;
margin-top: 1.5rem;
margin-bottom: 3rem;
}
.article-page .article-actions .article-meta .info {
text-align: left;
}
.article-page .comment-form .card-block {
padding: 0;
}
.article-page .comment-form .card-block textarea {
border: 0;
padding: 1.25rem;
}
.article-page .comment-form .card-footer .btn {
font-weight: 700;
float: right;
}
.article-page .comment-form .card-footer .comment-author-img {
height: 30px;
width: 30px;
}
.article-page .card {
border: 1px solid #eee;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.article-page .card .card-footer {
border-top: 1px solid #eee;
font-size: 0.8rem;
font-weight: 300;
}
.article-page .card .comment-author-img {
display: inline-block;
vertical-align: middle;
height: 20px;
width: 20px;
border-radius: 30px;
}
.article-page .card .comment-author {
display: inline-block;
vertical-align: middle;
}
.article-page .card .date-posted {
display: inline-block;
vertical-align: middle;
margin-left: 5px;
color: var(--text-light);
}
.article-page .card .mod-options {
float: right;
color: #333;
font-size: 1rem;
}
.article-page .card .mod-options i {
margin-left: 5px;
opacity: 0.6;
cursor: pointer;
}
.article-page .card .mod-options i:hover {
opacity: 1;
}
/* ============================================================================
* PROFILE PAGE
* ============================================================================ */
.profile-page .user-info {
text-align: center;
background: #fff;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
padding: 2rem 0 1rem 0;
}
.profile-page .user-info .user-img {
width: 100px;
height: 100px;
border-radius: 100px;
margin-bottom: 1rem;
}
.profile-page .user-info h4 {
font-weight: 700;
}
.profile-page .user-info p {
margin: 0 auto 0.5rem auto;
color: var(--text-lighter);
max-width: 450px;
font-weight: 300;
}
.profile-page .user-info .action-btn {
float: right;
color: var(--text-muted);
border: 1px solid var(--text-muted);
}
.profile-page .articles-toggle {
margin: 1.5rem 0 -1px 0;
}
/* ============================================================================
* EDITOR PAGE
* ============================================================================ */
.editor-page .tag-list i {
font-size: 0.6rem;
margin-right: 5px;
cursor: pointer;
}
================================================
FILE: docs/.gitignore
================================================
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
================================================
FILE: docs/.vscode/extensions.json
================================================
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
================================================
FILE: docs/.vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
================================================
FILE: docs/README.md
================================================
# Starlight Starter Kit: Basics
[](https://starlight.astro.build)
```
npm create astro@latest -- --template starlight
```
[](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics)
[](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics)
[](https://app.netlify.com/start/deploy?repository=https://github.com/withastro/starlight&create_from_path=examples/basics)
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs)
> 🧑🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro + Starlight project, you'll see the following folders and files:
```
.
├── public/
├── src/
│ ├── assets/
│ ├── content/
│ │ ├── docs/
│ │ └── config.ts
│ └── env.d.ts
├── astro.config.mjs
├── package.json
└── tsconfig.json
```
Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name.
Images can be added to `src/assets/` and embedded in Markdown with a relative link.
Static assets, like favicons, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat).
================================================
FILE: docs/astro.config.mjs
================================================
import {defineConfig} from 'astro/config';
import starlight from '@astrojs/starlight';
import tailwindcss from "@tailwindcss/vite";
/**
* A Vite plugin that removes `.md` extensions from URLs during the build process.
*
* This plugin is useful when working with Markdown files in a static site generator
* like Astro or Vite. It allows URLs to be served without the `.md` extension in
* the final build, making the URLs cleaner (e.g., `/docs/page` instead of `/docs/page.md`).
*
* ## Example Usage
* ```js
* import { defineConfig } from 'astro/config';
*
* export default defineConfig({
* vite: {
* plugins: [removeMdExtension()],
* },
* });
* ```
*
* @typedef {object} VitePlugin
* @property {string} name - The name of the plugin, in this case, `remove-md-extension`.
* @property {string} enforce - Specifies the plugin's enforcement stage, set to `pre`
* to ensure that this plugin runs before other transformations during the build.
* @property {function} transform - The function that processes each file, removing
* `.md` extensions from the file content. It is called on every file during the build process.
*
* @returns {VitePlugin} A Vite-compatible plugin object that contains the `name`,
* `enforce`, and `transform` properties, implementing the plugin functionality.
*
* ## Vite Plugin Object Structure
* - `name`: The name of the plugin (`remove-md-extension`).
* - `enforce`: Ensures the plugin runs early (`pre` stage).
* - `transform(code: string, id: string): string`: The function that processes the content of `.md` files.
*
* @param {string} code - The content of the file being processed (e.g., the raw Markdown).
* @param {string} id - The file identifier (usually the file path), used to check if the file ends in `.md`.
* @returns {string} The modified file content, with any `.md` extensions in URLs removed.
*/
function removeMdExtension() {
return {
name: 'remove-md-extension',
enforce: 'pre',
transform(code, id) {
if (id.endsWith('.md')) {
return code.replace(/\.md/g, '');
}
return code;
},
};
};
// https://astro.build/config
export default defineConfig({
integrations: [starlight({
title: 'RealWorld',
social: [
{
icon: 'github',
label: 'GitHub',
href: 'https://github.com/realworld-apps/realworld'
}
],
customCss: [
'./src/tailwind.css',
],
sidebar: [
{
label: 'Implementation creation',
items: [
{
label: 'Introduction',
slug: 'implementation-creation/introduction',
},
{
label: 'Features',
slug: 'implementation-creation/features',
},
{
label: 'Expectations',
slug: 'implementation-creation/expectations',
}
]
},
{
label: 'Specifications',
items: [
{
label: 'Frontend specifications',
items: [
{
label: 'Templates',
slug: 'specifications/frontend/templates',
},
{
label: 'Styles',
slug: 'specifications/frontend/styles',
},
{
label: 'Routing',
slug: 'specifications/frontend/routing',
},
{
label: 'API',
slug: 'specifications/frontend/api',
},
{
label: 'Tests',
slug: 'specifications/frontend/tests',
}
]
},
{
label: 'Backend specifications',
items: [
{
label: 'Introduction',
slug: 'specifications/backend/introduction',
},
{
label: 'Endpoints',
slug: 'specifications/backend/endpoints',
},
{
label: 'API response format',
slug: 'specifications/backend/api-response-format',
},
{
label: 'CORS',
slug: 'specifications/backend/cors',
},
{
label: 'Error handling',
slug: 'specifications/backend/error-handling',
},
{
label: 'Hurl',
slug: 'specifications/backend/hurl',
},
{
label: 'Tests',
slug: 'specifications/backend/tests',
}
]
},
{
label: 'Mobile specifications',
slug: 'specifications/mobile-specs/introduction'
}
]
},
{
label: 'Community',
items: [
// Each item here is one entry in the navigation menu.
{
label: 'Authors',
slug: 'community/authors',
},
{
label: 'Resources',
slug: 'community/resources',
},
{
label: 'Special Thanks',
slug: 'community/special-thanks',
}
]
}
]
})],
vite: {
plugins: [tailwindcss(), removeMdExtension()],
ssr: {
noExternal: ['@astrojs/starlight-tailwind'],
},
}
});
================================================
FILE: docs/non-included/LICENSES_LOGOS.md
================================================
# Logo Licenses & Trademark Analysis
> **Disclaimer**: This document was generated by Claude (Anthropic) and is provided
> for informational purposes only. It does not constitute legal advice. Consult a
> qualified attorney for legal guidance.
This document covers the licensing and trademark status of the framework logos
used in the animated SVG (`frameworks.svg`) of this repository.
## Individual Logo Licenses
### Angular
- **License**: CC BY 4.0 (Creative Commons Attribution 4.0 International)
- **Source**: [angular.dev/press-kit](https://angular.dev/press-kit)
- **Requirements**: Attribution to Google required
- **Trademark**: "ANGULAR" is a registered trademark of Google LLC
### React
- **License**: CC BY 4.0 (Creative Commons Attribution 4.0 International)
- **Source**: [github.com/facebook/react](https://github.com/facebook/react) / react.dev repo
- **Requirements**: Attribution to Meta required
- **Trademark**: React name and logo are trademarks of Meta Platforms, Inc.
- **Reference**: Confirmed in [GitHub issue #12570](https://github.com/facebook/react/issues/12570)
### Vue.js
- **License**: CC BY-NC-SA 4.0 (Creative Commons Attribution-NonCommercial-ShareAlike 4.0)
- **Source**: [github.com/vuejs/art](https://github.com/vuejs/art)
- **Requirements**: Attribution, non-commercial use, share-alike on derivatives
- **Permitted explicitly**: Open source / non-profit projects related to Vue.js
- **Trademark**: VUE.JS is a registered US trademark owned by Yuxi (Evan) You
### Django
- **License**: No open license — governed by DSF Trademark Policy
- **Source**: [djangoproject.com/community/logos](https://www.djangoproject.com/community/logos/)
- **Requirements**:
- Must not imply endorsement by the Django Software Foundation
- Must not modify the logo (colors, proportions, added text)
- **Trademark**: Registered trademark of the Django Software Foundation
- **Reference**: [djangoproject.com/trademarks](https://www.djangoproject.com/trademarks/)
### Hono
- **License**: MIT (as part of the honojs/hono repository)
- **Source**: [github.com/honojs/hono/docs/images](https://github.com/honojs/hono/tree/main/docs/images)
- **Requirements**: Include MIT copyright notice
- **Also available as**: CC0 1.0 (public domain) on third-party collections (gilbarbara/logos, Wikimedia Commons)
## Combined License of the Animated SVG
Since `frameworks.svg` is a derivative work incorporating all five logos, the
**most restrictive compatible terms** apply to the combined work.
### Governing constraints
| Constraint | Source | Impact |
|-------------------|----------------|-------------------------------------------------|
| **Attribution** | Angular, React, Vue, Hono | Credit all projects and their respective owners |
| **NonCommercial** | Vue.js (CC BY-NC-SA 4.0) | The animated SVG cannot be used commercially |
| **ShareAlike** | Vue.js (CC BY-NC-SA 4.0) | Derivatives of the SVG must use CC BY-NC-SA 4.0 or compatible |
| **No modification** | Django (trademark) | The Django logo within the SVG must remain unmodified (colors, proportions) |
| **Linking** | Django (trademark) | Logo should link to djangoproject.com "wherever technically possible" — not applicable here as the logo cycles within an animated SVG, making a dedicated hyperlink technically infeasible |
| **No endorsement** | All (trademark) | Must not imply official affiliation with any project |
### Effective license for this animated SVG
**CC BY-NC-SA 4.0**, with additional trademark constraints from Django.
This means:
- Free to share and adapt for **non-commercial** purposes
- Must give **attribution** to all five projects
- Derivatives must be shared under **the same license**
- The Django logo portion must remain **unmodified** and **link to djangoproject.com** when possible
- Usage must not suggest **endorsement** by any of the projects
### Risk assessment
| Logo | Risk | Notes |
|---------|-------|----------------------------------------------------------------|
| Angular | Low | CC BY 4.0 is very permissive |
| React | Low | CC BY 4.0 is very permissive |
| Vue.js | Low | Explicitly allows open-source project use |
| Django | Medium | Trademark-only; animation of the logo could be seen as modification. We use minimal fade-in/out animation only, keeping the logo visually unaltered |
| Hono | Low | MIT / CC0 — most permissive |
### Recommendation
The animated SVG is suitable for an open-source demo-apps repository, provided:
1. This `LICENSES_LOGOS.md` file is kept in the repository for attribution
2. The Django logo is not distorted, recolored, or visually altered (fade-in/out is acceptable)
3. The Django logo links to djangoproject.com when technically possible
4. The repository does not imply official endorsement by any of the five projects
## Attribution
- Angular logo by Google — [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- React logo by Meta Platforms, Inc. — [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)
- Vue.js logo by Evan You — [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
- Django logo by the Django Software Foundation — [Trademark Policy](https://www.djangoproject.com/trademarks/)
- Hono logo by Yusuke Wada — [MIT License](https://github.com/honojs/hono/blob/main/LICENSE)
---
*Generated by Claude (Anthropic)*
================================================
FILE: docs/package.json
================================================
{
"name": "documentation",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.6",
"@astrojs/react": "^4.4.2",
"@astrojs/starlight": "^0.37.4",
"@astrojs/starlight-tailwind": "^4.0.2",
"@tailwindcss/vite": "^4.1.8",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
"astro": "^5.16.16",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"sharp": "^0.34.5",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
}
================================================
FILE: docs/src/content/config.ts
================================================
import { defineCollection } from 'astro:content';
import { docsSchema } from '@astrojs/starlight/schema';
export const collections = {
docs: defineCollection({ schema: docsSchema() }),
};
================================================
FILE: docs/src/content/docs/community/authors.md
================================================
---
title: Authors
---
# Who currently maintains the project
#### [Gérôme Grignon](https://github.com/geromegrignon) - Maintainer
<img class="mr-4" align="left" width="40" height="40" src="https://avatars.githubusercontent.com/u/32737308?v=4">
Gérôme is a Software Engineer at Lucca. He's an open-source enthusiast.<br /><br />
#### [Manuel Vila](https://github.com/mvila) - Maintainer
<img class="mr-4" align="left" width="40" height="40" src="https://avatars.githubusercontent.com/u/381671?v=40">
Manuel is an independent Software Engineer, creator of the [Layr framework](https://layrjs.com) and the [CodebaseShow website](https://codebase.show/).<br /><br />
# Who created it
RealWorld would not be possible without the [open source community](https://realworld-docs.netlify.app/docs/community/special-thanks) continuously helping push the project forward. In addition, the former team was composed of:
#### [Anish Karandikar](https://github.com/anishkny) - Core Maintainer
<img class="mr-4" align="left" width="40" height="40" src="https://avatars1.githubusercontent.com/u/357499?v=3&s=100" />
MathWorker, ex-Google, ex-Computational Fluid Dynamicist, forever lover of tech & humanities ❤️
#### [Cameron Chapman](https://github.com/Cameron-C-Chapman) - Core Maintainer
<img class="mr-4" align="left" width="40" height="40" src="https://avatars1.githubusercontent.com/u/1323581?v=3&s=100" />
Cameron Chapman is a Software Engineer at FanThreeSixty. He's an open source enthusiast and is helping to teach a local web development boot camp at Kansas University.
#### [Eric Simons](https://twitter.com/ericsimons40) - Founder/Maintainer
<img class="mr-4" align="left" width="40" height="40" src="https://avatars1.githubusercontent.com/u/556934?v=3&s=100" />
Eric is a Software Engineer, UI Designer, and author of many technical books & tutorials. He oversees the project direction, maintenance and organizes the planning and development efforts of the team.
#### [Albert Pai](https://twitter.com/iamalbertpai) - Founder/Maintainer
<img class="mr-4" align="left" width="40" height="40" src="https://avatars0.githubusercontent.com/u/1776432?v=3&s=100" />
Albert is a Software Engineer, DevOps ninja, and author of many technical books & tutorials. He oversees the project direction, maintenance and organizes the planning and development efforts of the team.
#### [Thinkster](https://twitter.com/gothinkster) - Funded
<img class="mr-4" align="left" width="40" height="40" src="https://avatars0.githubusercontent.com/u/8601733?v=3&s=100" />
[Thinkster](https://x.com/GoThinkster) created high quality resources that helped Javascript developers succeed. The RealWorld project wouldn't exist without their funding!
#### [James Brewer](https://twitter.com/brwr_) - Admin
<img class="mr-4" align="left" width="40" height="40" src="https://avatars1.githubusercontent.com/u/4095660?v=3&s=100" />
James is a Software Engineer at Square and a contributor to the Django project. He created & maintained the RW Django codebase and continually provides guidance for the RealWorld project itself.
#### [Sandeesh S.](https://github.com/sandeesh) - Admin
<img class="mr-4" align="left" width="40" height="40" src="https://avatars1.githubusercontent.com/u/16877877?v=3&s=100" />
Full stack developer, Laravel enthusiast, Digital marketing specialist and an avid gamer.
================================================
FILE: docs/src/content/docs/community/resources.md
================================================
---
title: Resources
---
# Community created resources
- Performance comparisons:
- [A Real-World Comparison of Front-End Frameworks with Benchmarks 2020](https://medium.com/dailyjs/a-realworld-comparison-of-front-end-frameworks-2020-4e50655fe4c1)
- [A Real-World Comparison of Front-End Frameworks with Benchmarks 2019](https://medium.freecodecamp.org/a-realworld-comparison-of-front-end-frameworks-with-benchmarks-2019-update-4be0d3c78075)
- [A Real-World Comparison of Front-End Frameworks with Benchmarks 2018](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-2018-update-e5760fb4a962)
- [A Real-World Comparison of Front-End Frameworks with Benchmarks 2017](https://medium.freecodecamp.org/a-real-world-comparison-of-front-end-frameworks-with-benchmarks-e1cb62fd526c)
:::tip
Hello fellow writer, get in touch with us in [**GitHub Discussions**](https://github.com/realworld-apps/realworld/discussions/categories/community) so we can add your RealWorld related content here.
:::
================================================
FILE: docs/src/content/docs/community/special-thanks.md
================================================
---
title: Special thanks
---
RealWorld would not be possible without the open source community's assistance in reviewing codebases, developing new app implementations, and a variety of other duties that help the project progress. We'd like to thank the following OSS leaders for their contributions to RealWorld:
- **Dan Abramov** (creator of Redux) for helping [spark the initial idea](https://twitter.com/dan_abramov/status/692009757775896577), [getting the Redux community involved](https://github.com/reactjs/redux/issues/1353), as well as graciously taking the time to provide feedback on the Redux codebase
- **Max Lynch** (creator of Ionic) for taking the time to provide guidance in the early days of this project
- **Addy Osmani** (creator of TodoMVC) for helping [spark the initial idea](https://twitter.com/addyosmani/status/762828483433144320) and his amazing work with TodoMVC
- **TodoMVC** ([team & contributors](https://github.com/tastejs/todomvc#team)) for their exemplary & successful work; their project & org has been an invaluable analogy for us as we've built out RealWorld
- **James Brewer** (docs contributor to Django) for countless brainstorming sessions, helping name this project, and creating the Django codebase + tutorial
================================================
FILE: docs/src/content/docs/implementation-creation/expectations.md
================================================
---
title: Expectations
---
## Remember: Keep your codebases _simple_, yet _robust_.
If a new developer to your framework comes along and takes longer than 10 minutes to grasp the high-level architecture, it's likely that you went a little overboard in the engineering department.
Alternatively, you should _never_ forgo following fundamental best practices for the sake of simplicity, lest we teach that same newbie dev the _wrong_ way of doing things.
The quality & architecture of Conduit implementations should reflect something similar to an early-stage startup's MVP: functionally complete & stable, but not unnecessarily over-engineered.
## To write tests, or to not write tests?
**TL;DR** — we require a minimum of **one** unit test with every repo, but we'd definitely prefer all of them to include excellent testing coverage if the maintainers are willing to add it (or if someone in the community is kind enough to make a pull request :)
We believe that tests are a good concept, and we are big supporters of TDD in general. Building Conduit implementations without complete testing coverage, on the other hand, is a significant time commitment in and of itself, therefore we didn't include it in the spec at first since we believed that if people wanted it, it would be a fantastic "extra credit" aim for the repo. For example, a request for unit tests was made in our Angular 2 repo, and several fantastic community members are presently working on a PR to address it.
Another reason we didn’t include them in the spec is from the "Golden Rule" above:
> The quality & architecture of Conduit implementations should reflect something similar to an early-stage startup's MVP: functionally complete & stable, but not unnecessarily over-engineered.
Most startups we know that work in consumer-facing apps (like Conduit) don’t apply TDD/testing until they have a solid product-market fit, which is smart because they then spend most of their time iterating on product & UI and thus are far more likely to find PMF.
This doesn’t mean that TDD/testing === over-engineering, but in certain circumstances that statement does evaluate true (ex: consumer product finding PMF, side-projects, robust prototypes, etc).
That said, we do _prefer_ that every repo includes excellent tests that are exemplary of TDD/testing with that framework 👍
## Other Expectations
- All the required features (see specs) should be implemented.
- You should publish your implementation on a dedicated GitHub repository with the "Issues" section open.
- You should provide a README that presents an overview of your implementation and explains how to run it locally.
- The library/framework you are using should have at least 300 GitHub stars.
- You should do your best to keep your implementation up to date.
================================================
FILE: docs/src/content/docs/implementation-creation/features.md
================================================
---
title: Features
---
**General functionality:**
- Authenticate users via JWT (login/signup pages + logout button on settings page)
- CRU- users (sign up & settings page - no deleting required)
- CRUD Articles
- CR-D Comments on articles (no updating required)
- GET and display paginated lists of articles
- Favorite articles
- Follow other users
================================================
FILE: docs/src/content/docs/implementation-creation/introduction.md
================================================
---
title: Introduction
---
**Conduit** is a social blogging site (i.e. a Medium.com clone). It uses a custom API for all requests, including authentication.
:::tip
Check for [Discussions](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations) about works in progress as we don't list duplicate projects.
An opportunity to collaborate might await you already.
:::
Otherwise:
1. [fork our starter kit](https://github.com/gothinkster/realworld-starter-kit)
2. Read the following sections: _expectations_ and _features_ for a better understanding of this project
3. Read the frontend and/or the backend specs
4. Submit the new implementation on [CodebaseShow](https://codebase.show/projects/realworld)
**Happy coding!**
================================================
FILE: docs/src/content/docs/index.mdx
================================================
---
title: RealWorld apps
description: It's all about building real world, production ready apps.
template: splash
hero:
tagline: |
While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build real applications with it. That's why we, with the help of open source experts, design and serve as exemplary real world applications for each framework.
image:
file: ../../assets/img/realworld-logo.png
actions:
- text: Documentation
link: /introduction
icon: right-arrow
---
================================================
FILE: docs/src/content/docs/introduction.mdx
================================================
---
title: Introduction
---
# Introduction
> See how _the exact same_ Medium.com clone is built using different [frontends](https://codebase.show/projects/realworld?category=frontend) and [backends](https://codebase.show/projects/realworld?category=backend). Yes, you can mix and match them, because **they all adhere to the same [API spec](/specifications/backend/introduction)** 😮😎
While most "todo" demos provide an excellent cursory glance at a framework's capabilities, they typically don't convey the knowledge & perspective required to actually build _real_ applications with it.
**RealWorld** solves this by allowing you to choose any frontend (React, Angular, & more) and any backend (Node, Django, & more).
_Read the [full blog post announcing RealWorld on Medium.](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5)_
Join us on [GitHub Discussions!](https://github.com/realworld-apps/realworld/discussions) 🎉
## Implementations
Over 150 implementations have been created using various languages, libraries, and frameworks.
Explore them on [**CodebaseShow**](https://codebase.show/projects/realworld).
## Create a new implementation
[**Create a new implementation >>>**](implementation-creation/introduction)
Or you can [view upcoming implementations (WIPs)](https://github.com/realworld-apps/realworld/discussions/categories/wip-implementations).
## Learn more
- ["Introducing RealWorld 🙌"](https://medium.com/@ericsimons/introducing-realworld-6016654d36b5) by Eric Simons
- Every tutorial is built against the same [API spec](/specifications/backend/introduction) to ensure modularity of every frontend & backend
- Every frontend utilizes the same hand crafted [Bootstrap 4 theme](https://github.com/gothinkster/conduit-bootstrap-template) for identical UI/UX
- There is a [hosted version](https://realworld-docs.netlify.app/docs/specs/frontend-specs/api#demo-api) of the backend API available for public usage, no API keys are required
- Interested in creating a new RealWorld stack? View our [starter guide & spec](/implementation-creation/introduction)
================================================
FILE: docs/src/content/docs/specifications/backend/api-response-format.md
================================================
---
title: API response format
---
## JSON Objects returned by API:
Make sure the right content type like `Content-Type: application/json; charset=utf-8` is correctly returned.
### Users (for authentication)
```json
{
"user": {
"email": "jake@jake.jake",
"token": "jwt.token.here",
"username": "jake",
"bio": null,
"image": null
}
}
```
### Profile
```json
{
"profile": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://api.realworld.io/images/smiley-cyrus.jpg",
"following": false
}
}
```
### Single Article
```json
{
"article": {
"slug": "how-to-train-your-dragon",
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "It takes a Jacobian",
"tagList": ["dragons", "training"],
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:48:35.824Z",
"favorited": false,
"favoritesCount": 0,
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}
}
```
### Multiple Articles
:::caution
Starting from the 2024/08/16, the endpoints retrieving a list of articles do no longer return the body of an article for performance reasons.
It affcts:
- `GET /api/articles`
- `GET /api/articles/feed`
:::
```json
{
"articles":[{
"slug": "how-to-train-your-dragon",
"title": "How to train your dragon",
"description": "Ever wonder how?",
"tagList": ["dragons", "training"],
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:48:35.824Z",
"favorited": false,
"favoritesCount": 0,
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}, {
"slug": "how-to-train-your-dragon-2",
"title": "How to train your dragon 2",
"description": "So toothless",
"tagList": ["dragons", "training"],
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:48:35.824Z",
"favorited": false,
"favoritesCount": 0,
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}],
"articlesCount": 2
}
```
### Single Comment
```json
{
"comment": {
"id": 1,
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:22:56.637Z",
"body": "It takes a Jacobian",
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}
}
```
### Multiple Comments
```json
{
"comments": [{
"id": 1,
"createdAt": "2016-02-18T03:22:56.637Z",
"updatedAt": "2016-02-18T03:22:56.637Z",
"body": "It takes a Jacobian",
"author": {
"username": "jake",
"bio": "I work at statefarm",
"image": "https://i.stack.imgur.com/xHWG8.jpg",
"following": false
}
}]
}
```
### List of Tags
```json
{
"tags": [
"reactjs",
"angularjs"
]
}
```
================================================
FILE: docs/src/content/docs/specifications/backend/bruno.md
================================================
---
title: Bruno
---
For your convenience, we have a [Bruno collection](https://github.com/realworld-apps/realworld/tree/main/specs/api/bruno) that you can use to test your API endpoints as you build your app. You can run them all with [`run-api-tests-bruno.sh`](https://github.com/realworld-apps/realworld/blob/main/specs/api/run-api-tests-bruno.sh), or open the `bruno/` folder in the [Bruno app](https://www.usebruno.com) to run requests interactively.
The Bruno collection is automatically generated from the [Hurl test suite](/specifications/backend/hurl), which is the source of truth. It is kept in sync via CI.
## Running API tests locally
To locally run the provided Bruno collection against your backend, follow instructions [here](https://github.com/realworld-apps/realworld/tree/main/specs/api).
================================================
FILE: docs/src/content/docs/specifications/backend/cors.md
================================================
---
title: CORS
---
## Considerations for your backend with [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)
If the backend is about to run on a different host/port than the frontend, make sure to handle `OPTIONS` too and return correct `Access-Control-Allow-Origin` and `Access-Control-Allow-Headers` (e.g. `Content-Type`).
================================================
FILE: docs/src/content/docs/specifications/backend/endpoints.md
================================================
---
title: Endpoints
---
### Authentication Header:
You can read the authentication header from the headers of the request
`Authorization: Token jwt.token.here`
### Authentication:
`POST /api/users/login`
Example request body:
```json
{
"user":{
"email": "jake@jake.jake",
"password": "jakejake"
}
}
```
No authentication required, returns a [User](/specifications/backend/api-response-format#users-for-authentication)
Required fields: `email`, `password`
### Registration:
`POST /api/users`
Example request body:
```json
{
"user":{
"username": "Jacob",
"email": "jake@jake.jake",
"password": "jakejake"
}
}
```
No authentication required, returns a [User](/specifications/backend/api-response-format#users-for-authentication)
Required fields: `email`, `username`, `password`
### Get Current User
`GET /api/user`
Authentication required, returns a [User](/specifications/backend/api-response-format#users-for-authentication) that's the current user
### Update User
`PUT /api/user`
Example request body:
```json
{
"user":{
"email": "jake@jake.jake",
"bio": "I like to skateboard",
"image": "https://i.stack.imgur.com/xHWG8.jpg"
}
}
```
Authentication required, returns the [User](/specifications/backend/api-response-format#users-for-authentication)
Accepted fields: `email`, `username`, `password`, `image`, `bio`
### Get Profile
`GET /api/profiles/:username`
Authentication optional, returns a [Profile](/specifications/backend/api-response-format#profile)
### Follow user
`POST /api/profiles/:username/follow`
Authentication required, returns a [Profile](/specifications/backend/api-response-format#profile)
No additional parameters required
### Unfollow user
`DELETE /api/profiles/:username/follow`
Authentication required, returns a [Profile](/specifications/backend/api-response-format#profile)
No additional parameters required
### List Articles
`GET /api/articles`
Returns most recent articles globally by default, provide `tag`, `author` or `favorited` query parameter to filter results
Query Parameters:
Filter by tag:
`?tag=AngularJS`
Filter by author:
`?author=jake`
Favorited by user:
`?favorited=jake`
Limit number of articles (default is 20):
`?limit=20`
Offset/skip number of articles (default is 0):
`?offset=0`
Authentication optional, will return [multiple articles](/specifications/backend/api-response-format#multiple-articles), ordered by most recent first
### Feed Articles
`GET /api/articles/feed`
Can also take `limit` and `offset` query parameters like [List Articles](/specifications/backend/api-response-format#list-articles)
Authentication required, will return [multiple articles](/specifications/backend/api-response-format#multiple-articles) created by followed users, ordered by most recent first.
### Get Article
`GET /api/articles/:slug`
No authentication required, will return [single article](/specifications/backend/api-response-format#single-article)
### Create Article
`POST /api/articles`
Example request body:
```json
{
"article": {
"title": "How to train your dragon",
"description": "Ever wonder how?",
"body": "You have to believe",
"tagList": ["reactjs", "angularjs", "dragons"]
}
}
```
Authentication required, will return an [Article](/specifications/backend/api-response-format#single-article)
Required fields: `title`, `description`, `body`
Optional fields: `tagList` as an array of Strings
### Update Article
`PUT /api/articles/:slug`
Example request body:
```json
{
"article": {
"title": "Did you train your dragon?"
}
}
```
Authentication required, returns the updated [Article](/specifications/backend/api-response-format#single-article)
Optional fields: `title`, `description`, `body`
The `slug` also gets updated when the `title` is changed
### Delete Article
`DELETE /api/articles/:slug`
Authentication required
### Add Comments to an Article
`POST /api/articles/:slug/comments`
Example request body:
```json
{
"comment": {
"body": "His name was my name too."
}
}
```
Authentication required, returns the created [Comment](/specifications/backend/api-response-format#single-comment)
Required field: `body`
### Get Comments from an Article
`GET /api/articles/:slug/comments`
Authentication optional, returns [multiple comments](/specifications/backend/api-response-format#multiple-comments)
### Delete Comment
`DELETE /api/articles/:slug/comments/:id`
Authentication required
### Favorite Article
`POST /api/articles/:slug/favorite`
Authentication required, returns the [Article](/specifications/backend/api-response-format#single-article)
No additional parameters required
### Unfavorite Article
`DELETE /api/articles/:slug/favorite`
Authentication required, returns the [Article](/specifications/backend/api-response-format#single-article)
No additional parameters required
### Get Tags
`GET /api/tags`
No authentication required, returns a [List of Tags](/specifications/backend/api-response-format#list-of-tags)
================================================
FILE: docs/src/content/docs/specifications/backend/error-handling.md
================================================
---
title: Error handling
---
### Errors and Status Codes
If a request fails any validations, expect a 422 and errors in the following format:
```json
{
"errors":{
"body": [
"can't be empty"
]
}
}
```
#### Other status codes:
401 for Unauthorized requests, when a request requires authentication but it isn't provided
403 for Forbidden requests, when a request may be valid but the user doesn't have permissions to perform the action
404 for Not found requests, when a resource can't be found to fulfill the request
================================================
FILE: docs/src/content/docs/specifications/backend/hurl.md
================================================
---
title: Hurl
---
For your convenience, we have a [Hurl collection](https://github.com/realworld-apps/realworld/tree/main/specs/api/hurl) that you can use to test your API endpoints as you build your app. You can run them all with [`run-api-tests-hurl.sh`](https://github.com/realworld-apps/realworld/blob/main/specs/api/run-api-tests-hurl.sh).
A [Bruno collection](/specifications/backend/bruno) is also available if you prefer a GUI-based workflow.
## Running API tests locally
To locally run the provided Hurl collection against your backend, follow instructions [here](https://github.com/realworld-apps/realworld/tree/main/specs/api).
================================================
FILE: docs/src/content/docs/specifications/backend/introduction.md
================================================
---
title: Introduction
---
All backend implementations need to adhere to our [API spec](https://github.com/realworld-apps/realworld/tree/main/specs/api). The full API is described in the [OpenAPI spec](https://github.com/realworld-apps/realworld/blob/main/specs/api/openapi.yml).
For your convenience, we have a [Hurl collection](https://github.com/realworld-apps/realworld/tree/main/specs/api/hurl) that you can use to test your API endpoints as you build your app. You can run them all with [`run-api-tests-hurl.sh`](https://github.com/realworld-apps/realworld/blob/main/specs/api/run-api-tests-hurl.sh).
Check out our [starter kit](https://github.com/gothinkster/realworld-starter-kit) to create a new implementation, please read [references to the API specs & testing](/specifications/backend/introduction) required for creating a new backend.
================================================
FILE: docs/src/content/docs/specifications/backend/postman.md
================================================
---
title: Postman
---
For your convenience, we have a [Postman collection](https://github.com/realworld-apps/realworld/blob/main/specs/api/legacy_Conduit.postman_collection.json) that you can use to test your API endpoints as you build your app.
## Running API tests locally
To locally run the provided Postman collection against your backend, follow instructions [here](https://github.com/realworld-apps/realworld/tree/main/specs/api).
================================================
FILE: docs/src/content/docs/specifications/backend/tests.md
================================================
---
title: Tests
---
Include _at least_ **one** unit test in your repo to demonstrate how testing works (full testing coverage is _not_ required!)
================================================
FILE: docs/src/content/docs/specifications/frontend/api.md
================================================
---
title: API
---
This project provides you different solutions to test your frontend implementation with an API by:
- [running our official backend implementation locally](#run-the-official-backend-implementation-locally)
- [using the API deployed for the official demo](#demo-api)
## Run the official backend implementation locally
The official backend implementation is open-sourced.
You can find the GitHub repository [here](https://github.com/realworld-apps/nitro-prisma-zod-realworld-example-app).
The Readme will provide you guidances to start the server locally.
## Demo API
This project provides you with a public hosted API to test your frontend implementations.
Point your API requests to `https://api.realworld.show/api` and you're good to go!
### API Usage
The API is freely available for public usage but its access is limited to RealWorld usage only: you won't be able to t consume it on its own but with a frontend application.
## API Limitations
:::info
To avoid the need for content moderation on the public API, the following limitations have been introduced in 2021
:::
The visibility of user content is limited :
- logged out users see only content created by demo accounts
- logged in users see only their content and the content created by demo accounts
================================================
FILE: docs/src/content/docs/specifications/frontend/routing.md
================================================
---
title: Routing
---
- Home page (URL: `/` )
- List of tags
- List of articles pulled from either Feed, Global, or by Tag
- Pagination for list of articles
- Sign in/Sign up pages (URL: `/login`, `/register` )
- Uses JWT (store the token in localStorage)
- Authentication can be easily switched to session/cookie based
- Settings page (URL: `/settings` )
- Editor page to create/edit articles (URL: `/editor`, `/editor/article-slug-here` )
- Article page (URL: `/article/article-slug-here` )
- Delete article button (only shown to article's author)
- Render markdown from server client side
- Comments section at bottom of page
- Delete comment button (only shown to comment's author)
- Profile page (URL: `/profile/:username`, `/profile/:username/favorites` )
- Show basic user info
- List of articles populated from author's created articles or author's favorited articles
================================================
FILE: docs/src/content/docs/specifications/frontend/styles.md
================================================
---
title: Styles
---
All frontend implementations should use the shared [styles.css](https://github.com/realworld-apps/realworld/blob/main/assets/theme/styles.css) file from the main repository. This is a self-contained CSS file (Conduit Minimal CSS v4) that includes only the classes actually used by Conduit.
The CSS classes it provides match the [templates](/specifications/frontend/templates) and the [E2E test selectors contract](https://github.com/realworld-apps/realworld/blob/main/specs/e2e/SELECTORS.md).
### Default Avatar
When a user has no profile image, implementations should display the [default avatar](https://github.com/realworld-apps/realworld/blob/main/assets/media/default-avatar.svg) (a smiley face icon).
================================================
FILE: docs/src/content/docs/specifications/frontend/templates.md
================================================
---
title: Templates
---
## Head
The `<head>` element includes all the metadata for a page, including the title, description, and links to stylesheets and scripts.
```html
<head>
<meta charset="utf-8" />
<title>Conduit</title>
<!-- Import Ionicon icons & Google Fonts our Bootstrap theme relies on -->
<link
href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
rel="stylesheet"
type="text/css"
/>
<link
href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
rel="stylesheet"
type="text/css"
/>
<!-- Import the custom Bootstrap 4 theme from our hosted CDN -->
<link rel="stylesheet" href="//demo.productionready.io/main.css" />
</head>
```
## Layout
### Header
#### Unauthenticated user
If no user is logged in, then the header should include links to:
- the home page
- the login page
- the register page
> the link of the active page should use the **active** css class.
```html
<nav class="navbar navbar-light">
<div class="container">
<a class="navbar-brand" href="/">conduit</a>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!-- Add "active" class when you're on that page" -->
<a class="nav-link active" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/login">Sign in</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/register">Sign up</a>
</li>
</ul>
</div>
</nav>
```
#### Authenticated user
If a user is logged in, then the header should include links to:
- the home page
- the new article page
- the settings page
- the profile page
> the link of the active page should use the **active** css class.
```html
<nav class="navbar navbar-light">
<div class="container">
<a class="navbar-brand" href="/">conduit</a>
<ul class="nav navbar-nav pull-xs-right">
<li class="nav-item">
<!-- Add "active" class when you're on that page" -->
<a class="nav-link active" href="/">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/editor"> <i class="ion-compose"></i> New Article </a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings"> <i class="ion-gear-a"></i> Settings </a>
</li>
<li class="nav-item">
<a class="nav-link" href="/profile/eric-simons">
<img src="" class="user-pic" />
Eric Simons
</a>
</li>
</ul>
</div>
</nav>
```
### Footer
```html
<footer>
<div class="container">
<a href="/" class="logo-font">conduit</a>
<span class="attribution">
An interactive learning project. Code & design licensed under MIT.
</span>
</div>
</footer>
```
## Pages
### Home
The Home page includes up to three tabs:
- a default **Global Feed** tab
- an optional **tag name** tab, appears after clicking one of the popular tags
- an optional **Your Feed** tab, appears after logging in
```html
<div class="home-page">
<div class="banner">
<div class="container">
<h1 class="logo-font">conduit</h1>
<p>A place to share your knowledge.</p>
</div>
</div>
<div class="container page">
<div class="row">
<div class="col-md-9">
<div class="feed-toggle">
<ul class="nav nav-pills outline-active">
<li class="nav-item">
<a class="nav-link" href="">Your Feed</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="">Global Feed</a>
</li>
</ul>
</div>
<div class="article-preview">
<div class="article-meta">
<a href="/profile/eric-simons"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
<div class="info">
<a href="/profile/eric-simons" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 29
</button>
</div>
<a href="/article/how-to-build-webapps-that-scale" class="preview-link">
<h1>How to build webapps that scale</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
<ul class="tag-list">
<li class="tag-default tag-pill tag-outline">realworld</li>
<li class="tag-default tag-pill tag-outline">implementations</li>
</ul>
</a>
</div>
<div class="article-preview">
<div class="article-meta">
<a href="/profile/albert-pai"><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
<div class="info">
<a href="/profile/albert-pai" class="author">Albert Pai</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 32
</button>
</div>
<a href="/article/the-song-you" class="preview-link">
<h1>The song you won't ever stop singing. No matter how hard you try.</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
<ul class="tag-list">
<li class="tag-default tag-pill tag-outline">realworld</li>
<li class="tag-default tag-pill tag-outline">implementations</li>
</ul>
</a>
</div>
<ul class="pagination">
<li class="page-item active">
<a class="page-link" href="">1</a>
</li>
<li class="page-item">
<a class="page-link" href="">2</a>
</li>
</ul>
</div>
<div class="col-md-3">
<div class="sidebar">
<p>Popular Tags</p>
<div class="tag-list">
<a href="" class="tag-pill tag-default">programming</a>
<a href="" class="tag-pill tag-default">javascript</a>
<a href="" class="tag-pill tag-default">emberjs</a>
<a href="" class="tag-pill tag-default">angularjs</a>
<a href="" class="tag-pill tag-default">react</a>
<a href="" class="tag-pill tag-default">mean</a>
<a href="" class="tag-pill tag-default">node</a>
<a href="" class="tag-pill tag-default">rails</a>
</div>
</div>
</div>
</div>
</div>
</div>
```
### Authentication
#### Login
```html
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Sign in</h1>
<p class="text-xs-center">
<a href="/register">Need an account?</a>
</p>
<ul class="error-messages">
<li>That email is already taken</li>
</ul>
<form>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" />
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password" />
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">Sign in</button>
</form>
</div>
</div>
</div>
</div>
```
#### Register
```html
<div class="auth-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Sign up</h1>
<p class="text-xs-center">
<a href="/login">Have an account?</a>
</p>
<ul class="error-messages">
<li>That email is already taken</li>
</ul>
<form>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Username" />
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" />
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="password" placeholder="Password" />
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">Sign up</button>
</form>
</div>
</div>
</div>
</div>
```
### Profile
```html
<div class="profile-page">
<div class="user-info">
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-10 offset-md-1">
<img src="http://i.imgur.com/Qr71crq.jpg" class="user-img" />
<h4>Eric Simons</h4>
<p>
Cofounder @GoThinkster, lived in Aol's HQ for a few months, kinda looks like Peeta from
the Hunger Games
</p>
<button class="btn btn-sm btn-outline-secondary action-btn">
<i class="ion-plus-round"></i>
Follow Eric Simons
</button>
<button class="btn btn-sm btn-outline-secondary action-btn">
<i class="ion-gear-a"></i>
Edit Profile Settings
</button>
</div>
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-10 offset-md-1">
<div class="articles-toggle">
<ul class="nav nav-pills outline-active">
<li class="nav-item">
<a class="nav-link active" href="">My Articles</a>
</li>
<li class="nav-item">
<a class="nav-link" href="">Favorited Articles</a>
</li>
</ul>
</div>
<div class="article-preview">
<div class="article-meta">
<a href="/profile/eric-simons"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
<div class="info">
<a href="/profile/eric-simons" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 29
</button>
</div>
<a href="/article/how-to-buil-webapps-that-scale" class="preview-link">
<h1>How to build webapps that scale</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
<ul class="tag-list">
<li class="tag-default tag-pill tag-outline">realworld</li>
<li class="tag-default tag-pill tag-outline">implementations</li>
</ul>
</a>
</div>
<div class="article-preview">
<div class="article-meta">
<a href="/profile/albert-pai"><img src="http://i.imgur.com/N4VcUeJ.jpg" /></a>
<div class="info">
<a href="/profile/albert-pai" class="author">Albert Pai</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-outline-primary btn-sm pull-xs-right">
<i class="ion-heart"></i> 32
</button>
</div>
<a href="/article/the-song-you" class="preview-link">
<h1>The song you won't ever stop singing. No matter how hard you try.</h1>
<p>This is the description for the post.</p>
<span>Read more...</span>
<ul class="tag-list">
<li class="tag-default tag-pill tag-outline">Music</li>
<li class="tag-default tag-pill tag-outline">Song</li>
</ul>
</a>
</div>
<ul class="pagination">
<li class="page-item active">
<a class="page-link" href="">1</a>
</li>
<li class="page-item">
<a class="page-link" href="">2</a>
</li>
</ul>
</div>
</div>
</div>
</div>
```
### Settings
```html
<div class="settings-page">
<div class="container page">
<div class="row">
<div class="col-md-6 offset-md-3 col-xs-12">
<h1 class="text-xs-center">Your Settings</h1>
<ul class="error-messages">
<li>That name is required</li>
</ul>
<form>
<fieldset>
<fieldset class="form-group">
<input class="form-control" type="text" placeholder="URL of profile picture" />
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Your Name" />
</fieldset>
<fieldset class="form-group">
<textarea
class="form-control form-control-lg"
rows="8"
placeholder="Short bio about you"
></textarea>
</fieldset>
<fieldset class="form-group">
<input class="form-control form-control-lg" type="text" placeholder="Email" />
</fieldset>
<fieldset class="form-group">
<input
class="form-control form-control-lg"
type="password"
placeholder="New Password"
/>
</fieldset>
<button class="btn btn-lg btn-primary pull-xs-right">Update Settings</button>
</fieldset>
</form>
<hr />
<button class="btn btn-outline-danger">Or click here to logout.</button>
</div>
</div>
</div>
</div>
```
### Create/Edit Article
```html
<div class="editor-page">
<div class="container page">
<div class="row">
<div class="col-md-10 offset-md-1 col-xs-12">
<ul class="error-messages">
<li>That title is required</li>
</ul>
<form>
<fieldset>
<fieldset class="form-group">
<input type="text" class="form-control form-control-lg" placeholder="Article Title" />
</fieldset>
<fieldset class="form-group">
<input type="text" class="form-control" placeholder="What's this article about?" />
</fieldset>
<fieldset class="form-group">
<textarea
class="form-control"
rows="8"
placeholder="Write your article (in markdown)"
></textarea>
</fieldset>
<fieldset class="form-group">
<input type="text" class="form-control" placeholder="Enter tags" />
<div class="tag-list">
<span class="tag-default tag-pill"> <i class="ion-close-round"></i> tag </span>
</div>
</fieldset>
<button class="btn btn-lg pull-xs-right btn-primary" type="button">
Publish Article
</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
```
### Article
// TODO : update to switch between follow/favorite AND edit/delete
```html
<div class="article-page">
<div class="banner">
<div class="container">
<h1>How to build webapps that scale</h1>
<div class="article-meta">
<a href="/profile/eric-simons"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
<div class="info">
<a href="/profile/eric-simons" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-sm btn-outline-secondary">
<i class="ion-plus-round"></i>
Follow Eric Simons <span class="counter">(10)</span>
</button>
<button class="btn btn-sm btn-outline-primary">
<i class="ion-heart"></i>
Favorite Post <span class="counter">(29)</span>
</button>
<button class="btn btn-sm btn-outline-secondary">
<i class="ion-edit"></i> Edit Article
</button>
<button class="btn btn-sm btn-outline-danger">
<i class="ion-trash-a"></i> Delete Article
</button>
</div>
</div>
</div>
<div class="container page">
<div class="row article-content">
<div class="col-md-12">
<p>
Web development technologies have evolved at an incredible clip over the past few years.
</p>
<h2 id="introducing-ionic">Introducing RealWorld.</h2>
<p>It's a great solution for learning how other frameworks work.</p>
<ul class="tag-list">
<li class="tag-default tag-pill tag-outline">realworld</li>
<li class="tag-default tag-pill tag-outline">implementations</li>
</ul>
</div>
</div>
<hr />
<div class="article-actions">
<div class="article-meta">
<a href="profile.html"><img src="http://i.imgur.com/Qr71crq.jpg" /></a>
<div class="info">
<a href="" class="author">Eric Simons</a>
<span class="date">January 20th</span>
</div>
<button class="btn btn-sm btn-outline-secondary">
<i class="ion-plus-round"></i>
Follow Eric Simons
</button>
<button class="btn btn-sm btn-outline-primary">
<i class="ion-heart"></i>
Favorite Article <span class="counter">(29)</span>
</button>
<button class="btn btn-sm btn-outline-secondary">
<i class="ion-edit"></i> Edit Article
</button>
<button class="btn btn-sm btn-outline-danger">
<i class="ion-trash-a"></i> Delete Article
</button>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-8 offset-md-2">
<form class="card comment-form">
<div class="card-block">
<textarea class="form-control" placeholder="Write a comment..." rows="3"></textarea>
</div>
<div class="card-footer">
<img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
<button class="btn btn-sm btn-primary">Post Comment</button>
</div>
</form>
<div class="card">
<div class="card-block">
<p class="card-text">
With supporting text below as a natural lead-in to additional content.
</p>
</div>
<div class="card-footer">
<a href="/profile/author" class="comment-author">
<img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
</a>
<a href="/profile/jacob-schmidt" class="comment-author">Jacob Schmidt</a>
<span class="date-posted">Dec 29th</span>
</div>
</div>
<div class="card">
<div class="card-block">
<p class="card-text">
With supporting text below as a natural lead-in to additional content.
</p>
</div>
<div class="card-footer">
<a href="/profile/author" class="comment-author">
<img src="http://i.imgur.com/Qr71crq.jpg" class="comment-author-img" />
</a>
<a href="/profile/jacob-schmidt" class="comment-author">Jacob Schmidt</a>
<span class="date-posted">Dec 29th</span>
<span class="mod-options">
<i class="ion-trash-a"></i>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
```
================================================
FILE: docs/src/content/docs/specifications/frontend/tests.md
================================================
---
title: Tests
---
Include _at least_ **one** unit test in your repo to demonstrate how testing works (full testing coverage is _not_ required!)
## E2E Tests
A shared [Playwright E2E test suite](https://github.com/realworld-apps/realworld/tree/main/specs/e2e) is available to validate your frontend implementation. It covers authentication, articles, comments, navigation, settings, social features, error handling, and even basic XSS security.
To make your implementation compatible with the E2E tests, it **must** follow the [selectors contract](https://github.com/realworld-apps/realworld/blob/main/specs/e2e/SELECTORS.md), which defines:
- Form input `name` attributes
- Required CSS classes (layout, feed, tags, comments, profile, pagination, buttons, errors)
- Required text content for buttons and links
- Routes
- A debug interface (`window.__conduit_debug__`)
- LocalStorage key for the JWT token
- Default avatar behavior
### Running the tests
The test suite ships with a [base Playwright config](https://github.com/realworld-apps/realworld/blob/main/specs/e2e/playwright.base.ts) that you can extend in your implementation. Override `baseURL` and `webServer` to point to your dev server.
See the [Angular implementation](https://github.com/realworld-apps/angular-realworld-example-app) for a working example.
================================================
FILE: docs/src/content/docs/specifications/mobile-specs/introduction.md
================================================
---
title: Introduction
---
### [Icons for (iOS/Android)](https://github.com/realworld-apps/realworld/tree/master/spec/mobile_icons)
### Styles/Templates
Unfortunately, there isn't a common way for us to reuse & share styles/templates for cross-platform mobile apps.
Instead, we recommend using the Medium.com [iOS](https://itunes.apple.com/us/app/medium/id828256236?mt=8) and [Android](https://play.google.com/store/apps/details?id=com.medium.reader&hl=en) apps as a "north star" regarding general UI functionality/layout, but try not to go too overboard otherwise it will unnecessarily complicate your codebase (in other words, [KISS](https://en.wikipedia.org/wiki/KISS_principle) :)
================================================
FILE: docs/src/env.d.ts
================================================
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
================================================
FILE: docs/src/tailwind.css
================================================
@import "tailwindcss";
@import "@astrojs/starlight-tailwind";
@source "../**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}";
@theme {
--color-accent-200: #e3b6ed;
--color-accent-600: #a700c3;
--color-accent-900: #4e0e5b;
--color-accent-950: #36113e;
--color-gray-100: #f8f4fe;
--color-gray-200: #f2e9fd;
--color-gray-300: #c7bdd5;
--color-gray-400: #9581ae;
--color-gray-500: #614e78;
--color-gray-700: #412e55;
--color-gray-800: #2f1c42;
--color-gray-900: #1c1425;
}
/* Restore list styles in content area */
.sl-markdown-content ul {
list-style-type: disc;
padding-left: 1.5rem;
}
.sl-markdown-content ol {
list-style-type: decimal;
padding-left: 1.5rem;
}
.sl-markdown-content ul ul,
.sl-markdown-content ol ul {
list-style-type: circle;
}
.sl-markdown-content ul ul ul,
.sl-markdown-content ol ol ul,
.sl-markdown-content ol ul ul {
list-style-type: square;
}
================================================
FILE: docs/tsconfig.json
================================================
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
================================================
FILE: specs/api/README.md
================================================
# RealWorld API Spec
## Running API tests locally
### With Hurl
To locally run the provided [Hurl](https://hurl.dev) collection against your backend, execute:
```
HOST=http://localhost:3000/api ./run-api-tests-hurl.sh
```
For more details, see [`run-api-tests-hurl.sh`](run-api-tests-hurl.sh).
### With Bruno
A [Bruno](https://www.usebruno.com) collection is also available, automatically generated from the Hurl test suite. To run it:
```
HOST=http://localhost:3000/api ./run-api-tests-bruno.sh
```
For more details, see [`run-api-tests-bruno.sh`](run-api-tests-bruno.sh).
You can also open the `bruno/` folder directly in the Bruno app to run and inspect requests interactively.
> **Note:** The Hurl files are the source of truth. The Bruno collection is generated with `make bruno-generate` and kept in sync via CI (`make bruno-check`).
================================================
FILE: specs/api/bruno/articles/01-setup-register.bru
================================================
meta {
name: Setup: Register
type: http
seq: 1
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "art_{{uid}}",
"email": "art_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/articles/02-create-article-with-tags.bru
================================================
meta {
name: Create article with tags
type: http
seq: 2
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Test Article {{uid}}",
"description": "Test description",
"body": "Test body content",
"tagList": ["d_{{uid}}", "t_{{uid}}"]
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug", res.body.article.slug);
bru.setVar("created_at", res.body.article.createdAt);
bru.setVar("updated_at", res.body.article.updatedAt);
expect(res.body.article.title).to.eql("Test Article " + bru.getVar("uid"));
expect(typeof res.body.article.slug).to.eql("string");
expect(res.body.article.description).to.eql("Test description");
expect(res.body.article.body).to.eql("Test body content");
expect(res.body.article.tagList).to.include("d_" + bru.getVar("uid"));
expect(res.body.article.tagList).to.include("t_" + bru.getVar("uid"));
expect(res.body.article.tagList[0]).to.eql("d_" + bru.getVar("uid"));
expect(res.body.article.tagList[1]).to.eql("t_" + bru.getVar("uid"));
expect(res.body.article.createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T/);
expect(res.body.article.updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T/);
expect(res.body.article.favorited).to.eql(false);
expect(res.body.article.favoritesCount).to.eql(0);
expect(res.body.article.author.username).to.eql("art_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/03-list-all-articles.bru
================================================
meta {
name: List all articles
type: http
seq: 3
}
get {
url: {{host}}/api/articles
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
expect(res.body.articlesCount).to.be.at.least(1);
expect(typeof res.body.articles[0].title).to.eql("string");
expect(typeof res.body.articles[0].slug).to.eql("string");
expect(typeof res.body.articles[0].description).to.eql("string");
expect(res.body.articles[0]).to.not.have.property("body");
expect(Array.isArray(res.body.articles[0].tagList)).to.eql(true);
expect(res.body.articles[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.articles[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.articles[0].favorited).to.eql("boolean");
expect(Number.isInteger(res.body.articles[0].favoritesCount)).to.eql(true);
expect(typeof res.body.articles[0].author.username).to.eql("string");
}
================================================
FILE: specs/api/bruno/articles/04-list-by-author.bru
================================================
meta {
name: List by author
type: http
seq: 4
}
get {
url: {{host}}/api/articles?author=art_{{uid}}
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
expect(res.body.articlesCount).to.be.at.least(1);
expect(typeof res.body.articles[0].title).to.eql("string");
expect(typeof res.body.articles[0].slug).to.eql("string");
expect(typeof res.body.articles[0].description).to.eql("string");
expect(res.body.articles[0]).to.not.have.property("body");
expect(Array.isArray(res.body.articles[0].tagList)).to.eql(true);
expect(res.body.articles[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.articles[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.articles[0].favorited).to.eql("boolean");
expect(Number.isInteger(res.body.articles[0].favoritesCount)).to.eql(true);
expect(res.body.articles[0].author.username).to.eql("art_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/05-list-all-articles-with-auth.bru
================================================
meta {
name: List all articles with auth
type: http
seq: 5
}
get {
url: {{host}}/api/articles
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
expect(res.body.articlesCount).to.be.at.least(1);
expect(typeof res.body.articles[0].title).to.eql("string");
expect(typeof res.body.articles[0].slug).to.eql("string");
expect(typeof res.body.articles[0].description).to.eql("string");
expect(res.body.articles[0]).to.not.have.property("body");
expect(Array.isArray(res.body.articles[0].tagList)).to.eql(true);
expect(res.body.articles[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.articles[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.articles[0].favorited).to.eql("boolean");
expect(Number.isInteger(res.body.articles[0].favoritesCount)).to.eql(true);
expect(typeof res.body.articles[0].author.username).to.eql("string");
}
================================================
FILE: specs/api/bruno/articles/06-list-by-author-with-auth.bru
================================================
meta {
name: List by author with auth
type: http
seq: 6
}
get {
url: {{host}}/api/articles?author=art_{{uid}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
expect(res.body.articlesCount).to.be.at.least(1);
expect(typeof res.body.articles[0].title).to.eql("string");
expect(typeof res.body.articles[0].slug).to.eql("string");
expect(typeof res.body.articles[0].description).to.eql("string");
expect(res.body.articles[0]).to.not.have.property("body");
expect(Array.isArray(res.body.articles[0].tagList)).to.eql(true);
expect(res.body.articles[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.articles[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.articles[0].favorited).to.eql("boolean");
expect(Number.isInteger(res.body.articles[0].favoritesCount)).to.eql(true);
expect(res.body.articles[0].author.username).to.eql("art_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/07-list-by-tag.bru
================================================
meta {
name: List by tag
type: http
seq: 7
}
get {
url: {{host}}/api/articles?tag=d_{{uid}}
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
expect(res.body.articlesCount).to.be.at.least(1);
expect(typeof res.body.articles[0].title).to.eql("string");
expect(typeof res.body.articles[0].slug).to.eql("string");
expect(typeof res.body.articles[0].description).to.eql("string");
expect(res.body.articles[0]).to.not.have.property("body");
expect(Array.isArray(res.body.articles[0].tagList)).to.eql(true);
expect(res.body.articles[0].tagList).to.include("d_" + bru.getVar("uid"));
expect(res.body.articles[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.articles[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.articles[0].favorited).to.eql("boolean");
expect(Number.isInteger(res.body.articles[0].favoritesCount)).to.eql(true);
expect(typeof res.body.articles[0].author.username).to.eql("string");
}
================================================
FILE: specs/api/bruno/articles/08-list-articles-without-auth.bru
================================================
meta {
name: List articles without auth
type: http
seq: 8
}
get {
url: {{host}}/api/articles
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
}
================================================
FILE: specs/api/bruno/articles/09-get-single-article.bru
================================================
meta {
name: Get single article
type: http
seq: 9
}
get {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.article.title).to.eql("Test Article " + bru.getVar("uid"));
expect(res.body.article.slug).to.eql(bru.getVar("slug"));
expect(res.body.article.description).to.eql("Test description");
expect(res.body.article.body).to.eql("Test body content");
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.favorited).to.eql(false);
expect(res.body.article.favoritesCount).to.eql(0);
expect(res.body.article.author.username).to.eql("art_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/10-update-article-body.bru
================================================
meta {
name: Update article body
type: http
seq: 10
}
put {
url: {{host}}/api/articles/{{slug}}
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"body": "Updated body content"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.article.title).to.eql("Test Article " + bru.getVar("uid"));
expect(res.body.article.slug).to.eql(bru.getVar("slug"));
expect(res.body.article.description).to.eql("Test description");
expect(res.body.article.body).to.eql("Updated body content");
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.tagList.length).to.eql(2);
expect(res.body.article.tagList).to.include("d_" + bru.getVar("uid"));
expect(res.body.article.tagList).to.include("t_" + bru.getVar("uid"));
expect(res.body.article.createdAt).to.eql(bru.getVar("created_at"));
expect(res.body.article.updatedAt).to.not.eql(bru.getVar("updated_at"));
expect(typeof res.body.article.favorited).to.eql("boolean");
expect(Number.isInteger(res.body.article.favoritesCount)).to.eql(true);
expect(res.body.article.author.username).to.eql("art_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/11-verify-update-persisted.bru
================================================
meta {
name: Verify update persisted
type: http
seq: 11
}
get {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.article.title).to.eql("Test Article " + bru.getVar("uid"));
expect(res.body.article.slug).to.eql(bru.getVar("slug"));
expect(res.body.article.description).to.eql("Test description");
expect(res.body.article.body).to.eql("Updated body content");
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.tagList.length).to.eql(2);
expect(res.body.article.tagList).to.include("d_" + bru.getVar("uid"));
expect(res.body.article.tagList).to.include("t_" + bru.getVar("uid"));
expect(res.body.article.createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.article.favorited).to.eql("boolean");
expect(Number.isInteger(res.body.article.favoritesCount)).to.eql(true);
expect(res.body.article.author.username).to.eql("art_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/12-update-article-without-taglist-tags-should-be-preserved.bru
================================================
meta {
name: Update article without tagList: tags should be preserved
type: http
seq: 12
}
put {
url: {{host}}/api/articles/{{slug}}
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"body": "Body without touching tags"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.article.body).to.eql("Body without touching tags");
expect(res.body.article.tagList.length).to.eql(2);
expect(res.body.article.tagList).to.include("d_" + bru.getVar("uid"));
expect(res.body.article.tagList).to.include("t_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/articles/13-update-article-remove-all-tags-with-empty-array.bru
================================================
meta {
name: Update article: remove all tags with empty array
type: http
seq: 13
}
put {
url: {{host}}/api/articles/{{slug}}
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"tagList": []
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.tagList.length).to.eql(0);
}
================================================
FILE: specs/api/bruno/articles/14-verify-tags-were-actually-removed.bru
================================================
meta {
name: Verify tags were actually removed
type: http
seq: 14
}
get {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.tagList.length).to.eql(0);
}
================================================
FILE: specs/api/bruno/articles/15-update-article-taglist-null-should-be-rejected.bru
================================================
meta {
name: Update article: tagList null should be rejected
type: http
seq: 15
}
put {
url: {{host}}/api/articles/{{slug}}
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"tagList": null
}
}
}
assert {
res.status: eq 422
}
================================================
FILE: specs/api/bruno/articles/16-delete-article.bru
================================================
meta {
name: Delete article
type: http
seq: 16
}
delete {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/articles/17-verify-deletion.bru
================================================
meta {
name: Verify deletion
type: http
seq: 17
}
get {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/auth/01-register.bru
================================================
meta {
name: Register
type: http
seq: 1
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "auth_{{uid}}",
"email": "auth_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("reg_token", res.body.user.token);
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid"));
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "@test.com");
expect(res.body.user.bio).to.be.null;
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/02-login.bru
================================================
meta {
name: Login
type: http
seq: 2
}
post {
url: {{host}}/api/users/login
body: json
auth: none
}
body:json {
{
"user": {
"email": "auth_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
bru.setVar("token", res.body.user.token);
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid"));
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "@test.com");
expect(res.body.user.bio).to.be.null;
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/03-get-current-user.bru
================================================
meta {
name: Get current user
type: http
seq: 3
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid"));
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "@test.com");
expect(res.body.user.bio).to.be.null;
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/04-update-user.bru
================================================
meta {
name: Update user
type: http
seq: 4
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"bio": "Updated bio"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid"));
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "@test.com");
expect(res.body.user.bio).to.eql("Updated bio");
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/05-verify-update-persisted.bru
================================================
meta {
name: Verify update persisted
type: http
seq: 5
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid"));
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "@test.com");
expect(res.body.user.bio).to.eql("Updated bio");
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/06-update-user-bio-to-empty-string-should-normalize-to-null.bru
================================================
meta {
name: Update user bio to empty string - should normalize to null
type: http
seq: 6
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"bio": ""
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.bio).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/07-verify-empty-string-normalization-persisted.bru
================================================
meta {
name: Verify empty string normalization persisted
type: http
seq: 7
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.bio).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/08-restore-bio-then-set-to-null.bru
================================================
meta {
name: Restore bio then set to null
type: http
seq: 8
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"bio": "Temporary bio"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.bio).to.eql("Temporary bio");
}
================================================
FILE: specs/api/bruno/auth/09-update-user-bio-to-null-should-accept-for-nullable-field.bru
================================================
meta {
name: Update user bio to null - should accept for nullable field
type: http
seq: 9
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"bio": null
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.bio).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/10-verify-null-bio-persisted.bru
================================================
meta {
name: Verify null bio persisted
type: http
seq: 10
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.bio).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/11-restore-bio.bru
================================================
meta {
name: Restore bio
type: http
seq: 11
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"bio": "Updated bio"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid"));
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "@test.com");
expect(res.body.user.bio).to.eql("Updated bio");
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/12-update-user-image.bru
================================================
meta {
name: Update user image
type: http
seq: 12
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"image": "https://example.com/photo.jpg"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.eql("https://example.com/photo.jpg");
}
================================================
FILE: specs/api/bruno/auth/13-verify-image-update-persisted.bru
================================================
meta {
name: Verify image update persisted
type: http
seq: 13
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.eql("https://example.com/photo.jpg");
}
================================================
FILE: specs/api/bruno/auth/14-update-image-to-empty-string-should-normalize-to-null.bru
================================================
meta {
name: Update image to empty string - should normalize to null
type: http
seq: 14
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"image": ""
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/15-verify-image-empty-string-normalization-persisted.bru
================================================
meta {
name: Verify image empty string normalization persisted
type: http
seq: 15
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/16-set-image-then-update-to-null-should-accept-for-nullable-field.bru
================================================
meta {
name: Set image then update to null - should accept for nullable field
type: http
seq: 16
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"image": "https://example.com/temp.jpg"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.eql("https://example.com/temp.jpg");
}
================================================
FILE: specs/api/bruno/auth/17-put-user.bru
================================================
meta {
name: PUT user
type: http
seq: 17
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"image": null
}
}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/18-verify-null-image-persisted.bru
================================================
meta {
name: Verify null image persisted
type: http
seq: 18
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.image).to.be.null;
}
================================================
FILE: specs/api/bruno/auth/19-update-username-and-email.bru
================================================
meta {
name: Update username and email
type: http
seq: 19
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"username": "auth_{{uid}}_upd",
"email": "auth_{{uid}}_upd@test.com"
}
}
}
assert {
res.status: eq 200
}
script:post-response {
bru.setVar("updated_token", res.body.user.token);
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid") + "_upd");
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "_upd@test.com");
expect(res.body.user.bio).to.eql("Updated bio");
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/auth/20-verify-username-email-update-persisted.bru
================================================
meta {
name: Verify username/email update persisted
type: http
seq: 20
}
get {
url: {{host}}/api/user
body: none
auth: none
}
headers {
Authorization: Token {{updated_token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.user.username).to.eql("auth_" + bru.getVar("uid") + "_upd");
expect(res.body.user.email).to.eql("auth_" + bru.getVar("uid") + "_upd@test.com");
expect(res.body.user.bio).to.eql("Updated bio");
expect(res.body.user.image).to.be.null;
expect(typeof res.body.user.token).to.eql("string");
expect(res.body.user.token).to.not.eql("");
}
================================================
FILE: specs/api/bruno/bruno.json
================================================
{
"version": "1",
"name": "RealWorld API",
"type": "collection"
}
================================================
FILE: specs/api/bruno/collection.bru
================================================
script:pre-request {
if (!bru.getVar("uid")) {
bru.setVar("uid", Date.now().toString() + Math.random().toString(36).substring(2, 6));
}
}
================================================
FILE: specs/api/bruno/comments/01-setup-register.bru
================================================
meta {
name: Setup: Register
type: http
seq: 1
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "cmt_{{uid}}",
"email": "cmt_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/comments/02-setup-create-article.bru
================================================
meta {
name: Setup: Create article
type: http
seq: 2
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Comment Article {{uid}}",
"description": "For comments",
"body": "Article body"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug", res.body.article.slug);
}
================================================
FILE: specs/api/bruno/comments/03-create-comment.bru
================================================
meta {
name: Create comment
type: http
seq: 3
}
post {
url: {{host}}/api/articles/{{slug}}/comments
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"comment": {
"body": "Test comment body"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("comment_id", res.body.comment.id);
expect(Number.isInteger(res.body.comment.id)).to.eql(true);
expect(res.body.comment.body).to.eql("Test comment body");
expect(res.body.comment.createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.comment.updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.comment.author.username).to.eql("cmt_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/comments/04-list-comments.bru
================================================
meta {
name: List comments
type: http
seq: 4
}
get {
url: {{host}}/api/articles/{{slug}}/comments
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.comments)).to.eql(true);
expect(res.body.comments.length).to.eql(1);
expect(res.body.comments[0].id).to.eql(bru.getVar("comment_id"));
expect(res.body.comments[0].body).to.eql("Test comment body");
expect(res.body.comments[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.comments[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.comments[0].author.username).to.eql("cmt_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/comments/05-list-comments-without-auth.bru
================================================
meta {
name: List comments without auth
type: http
seq: 5
}
get {
url: {{host}}/api/articles/{{slug}}/comments
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.comments)).to.eql(true);
expect(res.body.comments.length).to.eql(1);
expect(Number.isInteger(res.body.comments[0].id)).to.eql(true);
expect(res.body.comments[0].body).to.eql("Test comment body");
expect(res.body.comments[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.comments[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.comments[0].author.username).to.eql("cmt_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/comments/06-delete-comment.bru
================================================
meta {
name: Delete comment
type: http
seq: 6
}
delete {
url: {{host}}/api/articles/{{slug}}/comments/{{comment_id}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/comments/07-verify-deletion.bru
================================================
meta {
name: Verify deletion
type: http
seq: 7
}
get {
url: {{host}}/api/articles/{{slug}}/comments
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.comments.length).to.eql(0);
}
================================================
FILE: specs/api/bruno/comments/08-selective-deletion-create-two-comments-delete-one-verify-the-other-remains.bru
================================================
meta {
name: Selective deletion: create two comments, delete one, verify the other remains
type: http
seq: 8
}
post {
url: {{host}}/api/articles/{{slug}}/comments
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"comment": {
"body": "First comment"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("first_comment_id", res.body.comment.id);
}
================================================
FILE: specs/api/bruno/comments/09-post-comments.bru
================================================
meta {
name: POST comments
type: http
seq: 9
}
post {
url: {{host}}/api/articles/{{slug}}/comments
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"comment": {
"body": "Second comment"
}
}
}
assert {
res.status: eq 201
}
================================================
FILE: specs/api/bruno/comments/10-verify-two-comments-exist.bru
================================================
meta {
name: Verify two comments exist
type: http
seq: 10
}
get {
url: {{host}}/api/articles/{{slug}}/comments
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.comments.length).to.eql(2);
}
================================================
FILE: specs/api/bruno/comments/11-delete-the-first-comment.bru
================================================
meta {
name: Delete the first comment
type: http
seq: 11
}
delete {
url: {{host}}/api/articles/{{slug}}/comments/{{first_comment_id}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/comments/12-verify-only-the-second-comment-remains.bru
================================================
meta {
name: Verify only the second comment remains
type: http
seq: 12
}
get {
url: {{host}}/api/articles/{{slug}}/comments
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.comments.length).to.eql(1);
expect(res.body.comments[0].body).to.eql("Second comment");
}
================================================
FILE: specs/api/bruno/comments/13-cleanup.bru
================================================
meta {
name: Cleanup
type: http
seq: 13
}
delete {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/environments/local.bru
================================================
vars {
host: http://localhost:3000
}
================================================
FILE: specs/api/bruno/errors-articles/01-create-article-no-auth.bru
================================================
meta {
name: Create article no auth
type: http
seq: 1
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
body:json {
{
"article": {
"title": "No Auth Article",
"description": "test",
"body": "test"
}
}
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-articles/02-get-unknown-slug.bru
================================================
meta {
name: GET unknown slug
type: http
seq: 2
}
get {
url: {{host}}/api/articles/unknown-slug-{{uid}}
body: none
auth: none
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-articles/03-update-no-auth.bru
================================================
meta {
name: Update no auth
type: http
seq: 3
}
put {
url: {{host}}/api/articles/some-slug
body: json
auth: none
}
body:json {
{
"article": {
"body": "test"
}
}
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-articles/04-delete-no-auth.bru
================================================
meta {
name: Delete no auth
type: http
seq: 4
}
delete {
url: {{host}}/api/articles/some-slug
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-articles/05-get-feed-no-auth.bru
================================================
meta {
name: GET feed no auth
type: http
seq: 5
}
get {
url: {{host}}/api/articles/feed
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-articles/06-favorite-no-auth.bru
================================================
meta {
name: Favorite no auth
type: http
seq: 6
}
post {
url: {{host}}/api/articles/some-slug/favorite
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-articles/07-unfavorite-no-auth.bru
================================================
meta {
name: Unfavorite no auth
type: http
seq: 7
}
delete {
url: {{host}}/api/articles/some-slug/favorite
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-articles/08-setup-register-for-authenticated-error-tests.bru
================================================
meta {
name: Setup: Register for authenticated error tests
type: http
seq: 8
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ea_art_{{uid}}",
"email": "ea_art_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/errors-articles/09-create-article-empty-title.bru
================================================
meta {
name: Create article empty title
type: http
seq: 9
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "",
"description": "test",
"body": "test"
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.title[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-articles/10-create-article-empty-description.bru
================================================
meta {
name: Create article empty description
type: http
seq: 10
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Err Desc {{uid}}",
"description": "",
"body": "test"
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.description[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-articles/11-create-article-empty-body.bru
================================================
meta {
name: Create article empty body
type: http
seq: 11
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Err Body {{uid}}",
"description": "test",
"body": ""
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.body[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-articles/12-duplicate-titles-are-allowed-each-gets-a-unique-slug.bru
================================================
meta {
name: Duplicate titles are allowed (each gets a unique slug)
type: http
seq: 12
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Dup Title {{uid}}",
"description": "first",
"body": "first"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug1", res.body.article.slug);
}
================================================
FILE: specs/api/bruno/errors-articles/13-post-articles.bru
================================================
meta {
name: POST articles
type: http
seq: 13
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Dup Title {{uid}}",
"description": "second",
"body": "second"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug2", res.body.article.slug);
expect(res.body.article.slug).to.not.eql(bru.getVar("slug1"));
}
================================================
FILE: specs/api/bruno/errors-articles/14-update-unknown-slug.bru
================================================
meta {
name: Update unknown slug
type: http
seq: 14
}
put {
url: {{host}}/api/articles/unknown-slug-{{uid}}
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"body": "test"
}
}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-articles/15-favorite-unknown-slug.bru
================================================
meta {
name: Favorite unknown slug
type: http
seq: 15
}
post {
url: {{host}}/api/articles/unknown-slug-{{uid}}/favorite
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-articles/16-unfavorite-unknown-slug.bru
================================================
meta {
name: Unfavorite unknown slug
type: http
seq: 16
}
delete {
url: {{host}}/api/articles/unknown-slug-{{uid}}/favorite
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-articles/17-update-unknown-slug.bru
================================================
meta {
name: Update unknown slug
type: http
seq: 17
}
put {
url: {{host}}/api/articles/unknown-slug-{{uid}}
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"body": "test"
}
}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-articles/18-delete-unknown-slug.bru
================================================
meta {
name: Delete unknown slug
type: http
seq: 18
}
delete {
url: {{host}}/api/articles/unknown-slug-{{uid}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-articles/19-cleanup.bru
================================================
meta {
name: Cleanup
type: http
seq: 19
}
delete {
url: {{host}}/api/articles/{{slug1}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/errors-articles/20-delete-slug2.bru
================================================
meta {
name: DELETE {{slug2}}
type: http
seq: 20
}
delete {
url: {{host}}/api/articles/{{slug2}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/errors-auth/01-register-empty-username.bru
================================================
meta {
name: Register empty username
type: http
seq: 1
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "",
"email": "ea_blank_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.username[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-auth/02-register-empty-email.bru
================================================
meta {
name: Register empty email
type: http
seq: 2
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ea_blank_{{uid}}",
"email": "",
"password": "password123"
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.email[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-auth/03-register-empty-password.bru
================================================
meta {
name: Register empty password
type: http
seq: 3
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ea_blankp_{{uid}}",
"email": "ea_blankp_{{uid}}@test.com",
"password": ""
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.password[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-auth/04-register-valid-user-for-duplicate-and-login-tests.bru
================================================
meta {
name: Register valid user for duplicate and login tests
type: http
seq: 4
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ea_dup_{{uid}}",
"email": "ea_dup_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/errors-auth/05-register-duplicate-username.bru
================================================
meta {
name: Register duplicate username
type: http
seq: 5
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ea_dup_{{uid}}",
"email": "ea_dup2_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 409
}
script:post-response {
expect(res.body.errors.username[0]).to.eql("has already been taken");
}
================================================
FILE: specs/api/bruno/errors-auth/06-register-duplicate-email.bru
================================================
meta {
name: Register duplicate email
type: http
seq: 6
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ea_dup2_{{uid}}",
"email": "ea_dup_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 409
}
script:post-response {
expect(res.body.errors.email[0]).to.eql("has already been taken");
}
================================================
FILE: specs/api/bruno/errors-auth/07-login-empty-email.bru
================================================
meta {
name: Login empty email
type: http
seq: 7
}
post {
url: {{host}}/api/users/login
body: json
auth: none
}
body:json {
{
"user": {
"email": "",
"password": "password123"
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.email[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-auth/08-login-empty-password.bru
================================================
meta {
name: Login empty password
type: http
seq: 8
}
post {
url: {{host}}/api/users/login
body: json
auth: none
}
body:json {
{
"user": {
"email": "ea_dup_{{uid}}@test.com",
"password": ""
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.password[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-auth/09-login-wrong-password.bru
================================================
meta {
name: Login wrong password
type: http
seq: 9
}
post {
url: {{host}}/api/users/login
body: json
auth: none
}
body:json {
{
"user": {
"email": "ea_dup_{{uid}}@test.com",
"password": "wrongpassword"
}
}
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.credentials[0]).to.eql("invalid");
}
================================================
FILE: specs/api/bruno/errors-auth/10-get-user-no-auth.bru
================================================
meta {
name: GET /user no auth
type: http
seq: 10
}
get {
url: {{host}}/api/user
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-auth/11-put-user-no-auth.bru
================================================
meta {
name: PUT /user no auth
type: http
seq: 11
}
put {
url: {{host}}/api/user
body: json
auth: none
}
body:json {
{
"user": {
"bio": "test"
}
}
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-auth/12-update-email-to-empty-string-should-reject.bru
================================================
meta {
name: Update email to empty string - should reject
type: http
seq: 12
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"email": ""
}
}
}
assert {
res.status: eq 422
}
================================================
FILE: specs/api/bruno/errors-auth/13-update-username-to-empty-string-should-reject.bru
================================================
meta {
name: Update username to empty string - should reject
type: http
seq: 13
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"username": ""
}
}
}
assert {
res.status: eq 422
}
================================================
FILE: specs/api/bruno/errors-auth/14-update-email-to-null-should-reject.bru
================================================
meta {
name: Update email to null - should reject
type: http
seq: 14
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"email": null
}
}
}
assert {
res.status: eq 422
}
================================================
FILE: specs/api/bruno/errors-auth/15-update-username-to-null-should-reject.bru
================================================
meta {
name: Update username to null - should reject
type: http
seq: 15
}
put {
url: {{host}}/api/user
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"user": {
"username": null
}
}
}
assert {
res.status: eq 422
}
================================================
FILE: specs/api/bruno/errors-authorization/01-register-user-a.bru
================================================
meta {
name: Register user A
type: http
seq: 1
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "authz_a_{{uid}}",
"email": "authz_a_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token_a", res.body.user.token);
}
================================================
FILE: specs/api/bruno/errors-authorization/02-register-user-b.bru
================================================
meta {
name: Register user B
type: http
seq: 2
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "authz_b_{{uid}}",
"email": "authz_b_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token_b", res.body.user.token);
}
================================================
FILE: specs/api/bruno/errors-authorization/03-user-a-creates-article.bru
================================================
meta {
name: User A creates article
type: http
seq: 3
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token_a}}
}
body:json {
{
"article": {
"title": "Authz Article {{uid}}",
"description": "test",
"body": "test"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug", res.body.article.slug);
}
================================================
FILE: specs/api/bruno/errors-authorization/04-user-b-tries-to-delete-403.bru
================================================
meta {
name: User B tries to delete -> 403
type: http
seq: 4
}
delete {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
headers {
Authorization: Token {{token_b}}
}
assert {
res.status: eq 403
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("forbidden");
}
================================================
FILE: specs/api/bruno/errors-authorization/05-user-b-tries-to-update-403.bru
================================================
meta {
name: User B tries to update -> 403
type: http
seq: 5
}
put {
url: {{host}}/api/articles/{{slug}}
body: json
auth: none
}
headers {
Authorization: Token {{token_b}}
}
body:json {
{
"article": {
"body": "hijacked"
}
}
}
assert {
res.status: eq 403
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("forbidden");
}
================================================
FILE: specs/api/bruno/errors-authorization/06-user-a-creates-a-comment-on-the-article.bru
================================================
meta {
name: User A creates a comment on the article
type: http
seq: 6
}
post {
url: {{host}}/api/articles/{{slug}}/comments
body: json
auth: none
}
headers {
Authorization: Token {{token_a}}
}
body:json {
{
"comment": {
"body": "A's comment"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("comment_id", res.body.comment.id);
}
================================================
FILE: specs/api/bruno/errors-authorization/07-user-b-tries-to-delete-a-s-comment-403.bru
================================================
meta {
name: User B tries to delete A's comment -> 403
type: http
seq: 7
}
delete {
url: {{host}}/api/articles/{{slug}}/comments/{{comment_id}}
body: none
auth: none
}
headers {
Authorization: Token {{token_b}}
}
assert {
res.status: eq 403
}
script:post-response {
expect(res.body.errors.comment[0]).to.eql("forbidden");
}
================================================
FILE: specs/api/bruno/errors-authorization/08-verify-comment-survived-the-failed-delete.bru
================================================
meta {
name: Verify comment survived the failed delete
type: http
seq: 8
}
get {
url: {{host}}/api/articles/{{slug}}/comments
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(res.body.comments.length).to.be.at.least(1);
expect(res.body.comments[0].body).to.eql("A's comment");
}
================================================
FILE: specs/api/bruno/errors-authorization/09-cleanup-user-a-deletes-article.bru
================================================
meta {
name: Cleanup: User A deletes article
type: http
seq: 9
}
delete {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
headers {
Authorization: Token {{token_a}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/errors-comments/01-post-comment-no-auth.bru
================================================
meta {
name: Post comment no auth
type: http
seq: 1
}
post {
url: {{host}}/api/articles/some-slug/comments
body: json
auth: none
}
body:json {
{
"comment": {
"body": "test"
}
}
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-comments/02-delete-comment-no-auth.bru
================================================
meta {
name: Delete comment no auth
type: http
seq: 2
}
delete {
url: {{host}}/api/articles/some-slug/comments/1
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-comments/03-setup-register-create-article.bru
================================================
meta {
name: Setup: Register + create article
type: http
seq: 3
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ec_{{uid}}",
"email": "ec_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/errors-comments/04-post-articles.bru
================================================
meta {
name: POST articles
type: http
seq: 4
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Err Comment Art {{uid}}",
"description": "test",
"body": "test"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug", res.body.article.slug);
}
================================================
FILE: specs/api/bruno/errors-comments/05-post-comment-empty-body.bru
================================================
meta {
name: Post comment empty body
type: http
seq: 5
}
post {
url: {{host}}/api/articles/{{slug}}/comments
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"comment": {
"body": ""
}
}
}
assert {
res.status: eq 422
}
script:post-response {
expect(res.body.errors.body[0]).to.eql("can't be blank");
}
================================================
FILE: specs/api/bruno/errors-comments/06-post-comment-on-unknown-article.bru
================================================
meta {
name: Post comment on unknown article
type: http
seq: 6
}
post {
url: {{host}}/api/articles/unknown-slug-{{uid}}/comments
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"comment": {
"body": "orphan"
}
}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-comments/07-get-comments-on-unknown-article.bru
================================================
meta {
name: Get comments on unknown article
type: http
seq: 7
}
get {
url: {{host}}/api/articles/unknown-slug-{{uid}}/comments
body: none
auth: none
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-comments/08-delete-comment-on-unknown-article.bru
================================================
meta {
name: Delete comment on unknown article
type: http
seq: 8
}
delete {
url: {{host}}/api/articles/unknown-slug-{{uid}}/comments/99999
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.article[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-comments/09-delete-non-existent-comment-on-existing-article.bru
================================================
meta {
name: Delete non-existent comment on existing article
type: http
seq: 9
}
delete {
url: {{host}}/api/articles/{{slug}}/comments/99999
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.comment[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-comments/10-cleanup.bru
================================================
meta {
name: Cleanup
type: http
seq: 10
}
delete {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 204
}
================================================
FILE: specs/api/bruno/errors-profiles/01-get-unknown-profile.bru
================================================
meta {
name: GET unknown profile
type: http
seq: 1
}
get {
url: {{host}}/api/profiles/unknown-user-{{uid}}
body: none
auth: none
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.profile[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-profiles/02-follow-no-auth.bru
================================================
meta {
name: Follow no auth
type: http
seq: 2
}
post {
url: {{host}}/api/profiles/unknown-user-{{uid}}/follow
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-profiles/03-unfollow-no-auth.bru
================================================
meta {
name: Unfollow no auth
type: http
seq: 3
}
delete {
url: {{host}}/api/profiles/unknown-user-{{uid}}/follow
body: none
auth: none
}
assert {
res.status: eq 401
}
script:post-response {
expect(res.body.errors.token[0]).to.eql("is missing");
}
================================================
FILE: specs/api/bruno/errors-profiles/04-setup-register-for-authenticated-404-tests.bru
================================================
meta {
name: Setup: Register for authenticated 404 tests
type: http
seq: 4
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "ep_{{uid}}",
"email": "ep_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/errors-profiles/05-follow-unknown-user-authed.bru
================================================
meta {
name: Follow unknown user (authed)
type: http
seq: 5
}
post {
url: {{host}}/api/profiles/unknown-user-{{uid}}/follow
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.profile[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/errors-profiles/06-unfollow-unknown-user-authed.bru
================================================
meta {
name: Unfollow unknown user (authed)
type: http
seq: 6
}
delete {
url: {{host}}/api/profiles/unknown-user-{{uid}}/follow
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 404
}
script:post-response {
expect(res.body.errors.profile[0]).to.eql("not found");
}
================================================
FILE: specs/api/bruno/favorites/01-setup-register.bru
================================================
meta {
name: Setup: Register
type: http
seq: 1
}
post {
url: {{host}}/api/users
body: json
auth: none
}
body:json {
{
"user": {
"username": "fav_{{uid}}",
"email": "fav_{{uid}}@test.com",
"password": "password123"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("token", res.body.user.token);
}
================================================
FILE: specs/api/bruno/favorites/02-setup-create-article.bru
================================================
meta {
name: Setup: Create article
type: http
seq: 2
}
post {
url: {{host}}/api/articles
body: json
auth: none
}
headers {
Authorization: Token {{token}}
}
body:json {
{
"article": {
"title": "Favorite Article {{uid}}",
"description": "For favorites",
"body": "Article body"
}
}
}
assert {
res.status: eq 201
}
script:post-response {
bru.setVar("slug", res.body.article.slug);
}
================================================
FILE: specs/api/bruno/favorites/03-favorite-article.bru
================================================
meta {
name: Favorite article
type: http
seq: 3
}
post {
url: {{host}}/api/articles/{{slug}}/favorite
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(typeof res.body.article.title).to.eql("string");
expect(typeof res.body.article.slug).to.eql("string");
expect(typeof res.body.article.description).to.eql("string");
expect(typeof res.body.article.body).to.eql("string");
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.favorited).to.eql(true);
expect(res.body.article.favoritesCount).to.eql(1);
expect(res.body.article.author.username).to.eql("fav_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/favorites/04-verify-favorite-persists.bru
================================================
meta {
name: Verify favorite persists
type: http
seq: 4
}
get {
url: {{host}}/api/articles/{{slug}}
body: none
auth: none
}
headers {
Authorization: Token {{token}}
}
assert {
res.status: eq 200
}
script:post-response {
expect(typeof res.body.article.title).to.eql("string");
expect(typeof res.body.article.slug).to.eql("string");
expect(typeof res.body.article.description).to.eql("string");
expect(typeof res.body.article.body).to.eql("string");
expect(Array.isArray(res.body.article.tagList)).to.eql(true);
expect(res.body.article.createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.article.favorited).to.eql(true);
expect(res.body.article.favoritesCount).to.eql(1);
expect(res.body.article.author.username).to.eql("fav_" + bru.getVar("uid"));
}
================================================
FILE: specs/api/bruno/favorites/05-articles-filtered-by-favorited-username.bru
================================================
meta {
name: Articles filtered by favorited username
type: http
seq: 5
}
get {
url: {{host}}/api/articles?favorited=fav_{{uid}}
body: none
auth: none
}
assert {
res.status: eq 200
}
script:post-response {
expect(Array.isArray(res.body.articles)).to.eql(true);
expect(Number.isInteger(res.body.articlesCount)).to.eql(true);
expect(res.body.articlesCount).to.be.at.least(1);
expect(typeof res.body.articles[0].title).to.eql("string");
expect(typeof res.body.articles[0].slug).to.eql("string");
expect(typeof res.body.articles[0].description).to.eql("string");
expect(res.body.articles[0]).to.not.have.property("body");
expect(Array.isArray(res.body.articles[0].tagList)).to.eql(true);
expect(res.body.articles[0].createdAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(res.body.articles[0].updatedAt).to.match(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/);
expect(typeof res.body.articles[0].favorited).to.eql("boolean");
expect(Number.isInteger(res.body.articles[0].favoritesCount)).to.eql(true);
expect(res.body.articles[0].favoritesCount).to.be.at.least(1);
}
================================================
gitextract_60zq8mk5/
├── .github/
│ ├── CODEOWNERS
│ ├── ISSUE_TEMPLATE/
│ │ ├── BUG_REPORT.yml
│ │ └── FEATURE_REQUEST.yml
│ ├── dependabot.yml
│ └── workflows/
│ ├── bruno-check.yml
│ ├── codeql.yml
│ ├── deploy-docs.yml
│ └── spammy-guardian.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── assets/
│ ├── media/
│ │ ├── conduit-logo.svg.generate.ts
│ │ └── mobile_icons/
│ │ ├── ios/
│ │ │ ├── AppIcon.appiconset/
│ │ │ │ └── Contents.json
│ │ │ └── README.md
│ │ └── watchkit/
│ │ └── AppIcon.appiconset/
│ │ └── Contents.json
│ └── theme/
│ └── styles.css
├── docs/
│ ├── .gitignore
│ ├── .vscode/
│ │ ├── extensions.json
│ │ └── launch.json
│ ├── README.md
│ ├── astro.config.mjs
│ ├── non-included/
│ │ └── LICENSES_LOGOS.md
│ ├── package.json
│ ├── src/
│ │ ├── content/
│ │ │ ├── config.ts
│ │ │ └── docs/
│ │ │ ├── community/
│ │ │ │ ├── authors.md
│ │ │ │ ├── resources.md
│ │ │ │ └── special-thanks.md
│ │ │ ├── implementation-creation/
│ │ │ │ ├── expectations.md
│ │ │ │ ├── features.md
│ │ │ │ └── introduction.md
│ │ │ ├── index.mdx
│ │ │ ├── introduction.mdx
│ │ │ └── specifications/
│ │ │ ├── backend/
│ │ │ │ ├── api-response-format.md
│ │ │ │ ├── bruno.md
│ │ │ │ ├── cors.md
│ │ │ │ ├── endpoints.md
│ │ │ │ ├── error-handling.md
│ │ │ │ ├── hurl.md
│ │ │ │ ├── introduction.md
│ │ │ │ ├── postman.md
│ │ │ │ └── tests.md
│ │ │ ├── frontend/
│ │ │ │ ├── api.md
│ │ │ │ ├── routing.md
│ │ │ │ ├── styles.md
│ │ │ │ ├── templates.md
│ │ │ │ └── tests.md
│ │ │ └── mobile-specs/
│ │ │ └── introduction.md
│ │ ├── env.d.ts
│ │ └── tailwind.css
│ └── tsconfig.json
└── specs/
├── api/
│ ├── README.md
│ ├── bruno/
│ │ ├── articles/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-create-article-with-tags.bru
│ │ │ ├── 03-list-all-articles.bru
│ │ │ ├── 04-list-by-author.bru
│ │ │ ├── 05-list-all-articles-with-auth.bru
│ │ │ ├── 06-list-by-author-with-auth.bru
│ │ │ ├── 07-list-by-tag.bru
│ │ │ ├── 08-list-articles-without-auth.bru
│ │ │ ├── 09-get-single-article.bru
│ │ │ ├── 10-update-article-body.bru
│ │ │ ├── 11-verify-update-persisted.bru
│ │ │ ├── 12-update-article-without-taglist-tags-should-be-preserved.bru
│ │ │ ├── 13-update-article-remove-all-tags-with-empty-array.bru
│ │ │ ├── 14-verify-tags-were-actually-removed.bru
│ │ │ ├── 15-update-article-taglist-null-should-be-rejected.bru
│ │ │ ├── 16-delete-article.bru
│ │ │ └── 17-verify-deletion.bru
│ │ ├── auth/
│ │ │ ├── 01-register.bru
│ │ │ ├── 02-login.bru
│ │ │ ├── 03-get-current-user.bru
│ │ │ ├── 04-update-user.bru
│ │ │ ├── 05-verify-update-persisted.bru
│ │ │ ├── 06-update-user-bio-to-empty-string-should-normalize-to-null.bru
│ │ │ ├── 07-verify-empty-string-normalization-persisted.bru
│ │ │ ├── 08-restore-bio-then-set-to-null.bru
│ │ │ ├── 09-update-user-bio-to-null-should-accept-for-nullable-field.bru
│ │ │ ├── 10-verify-null-bio-persisted.bru
│ │ │ ├── 11-restore-bio.bru
│ │ │ ├── 12-update-user-image.bru
│ │ │ ├── 13-verify-image-update-persisted.bru
│ │ │ ├── 14-update-image-to-empty-string-should-normalize-to-null.bru
│ │ │ ├── 15-verify-image-empty-string-normalization-persisted.bru
│ │ │ ├── 16-set-image-then-update-to-null-should-accept-for-nullable-field.bru
│ │ │ ├── 17-put-user.bru
│ │ │ ├── 18-verify-null-image-persisted.bru
│ │ │ ├── 19-update-username-and-email.bru
│ │ │ └── 20-verify-username-email-update-persisted.bru
│ │ ├── bruno.json
│ │ ├── collection.bru
│ │ ├── comments/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-setup-create-article.bru
│ │ │ ├── 03-create-comment.bru
│ │ │ ├── 04-list-comments.bru
│ │ │ ├── 05-list-comments-without-auth.bru
│ │ │ ├── 06-delete-comment.bru
│ │ │ ├── 07-verify-deletion.bru
│ │ │ ├── 08-selective-deletion-create-two-comments-delete-one-verify-the-other-remains.bru
│ │ │ ├── 09-post-comments.bru
│ │ │ ├── 10-verify-two-comments-exist.bru
│ │ │ ├── 11-delete-the-first-comment.bru
│ │ │ ├── 12-verify-only-the-second-comment-remains.bru
│ │ │ └── 13-cleanup.bru
│ │ ├── environments/
│ │ │ └── local.bru
│ │ ├── errors-articles/
│ │ │ ├── 01-create-article-no-auth.bru
│ │ │ ├── 02-get-unknown-slug.bru
│ │ │ ├── 03-update-no-auth.bru
│ │ │ ├── 04-delete-no-auth.bru
│ │ │ ├── 05-get-feed-no-auth.bru
│ │ │ ├── 06-favorite-no-auth.bru
│ │ │ ├── 07-unfavorite-no-auth.bru
│ │ │ ├── 08-setup-register-for-authenticated-error-tests.bru
│ │ │ ├── 09-create-article-empty-title.bru
│ │ │ ├── 10-create-article-empty-description.bru
│ │ │ ├── 11-create-article-empty-body.bru
│ │ │ ├── 12-duplicate-titles-are-allowed-each-gets-a-unique-slug.bru
│ │ │ ├── 13-post-articles.bru
│ │ │ ├── 14-update-unknown-slug.bru
│ │ │ ├── 15-favorite-unknown-slug.bru
│ │ │ ├── 16-unfavorite-unknown-slug.bru
│ │ │ ├── 17-update-unknown-slug.bru
│ │ │ ├── 18-delete-unknown-slug.bru
│ │ │ ├── 19-cleanup.bru
│ │ │ └── 20-delete-slug2.bru
│ │ ├── errors-auth/
│ │ │ ├── 01-register-empty-username.bru
│ │ │ ├── 02-register-empty-email.bru
│ │ │ ├── 03-register-empty-password.bru
│ │ │ ├── 04-register-valid-user-for-duplicate-and-login-tests.bru
│ │ │ ├── 05-register-duplicate-username.bru
│ │ │ ├── 06-register-duplicate-email.bru
│ │ │ ├── 07-login-empty-email.bru
│ │ │ ├── 08-login-empty-password.bru
│ │ │ ├── 09-login-wrong-password.bru
│ │ │ ├── 10-get-user-no-auth.bru
│ │ │ ├── 11-put-user-no-auth.bru
│ │ │ ├── 12-update-email-to-empty-string-should-reject.bru
│ │ │ ├── 13-update-username-to-empty-string-should-reject.bru
│ │ │ ├── 14-update-email-to-null-should-reject.bru
│ │ │ └── 15-update-username-to-null-should-reject.bru
│ │ ├── errors-authorization/
│ │ │ ├── 01-register-user-a.bru
│ │ │ ├── 02-register-user-b.bru
│ │ │ ├── 03-user-a-creates-article.bru
│ │ │ ├── 04-user-b-tries-to-delete-403.bru
│ │ │ ├── 05-user-b-tries-to-update-403.bru
│ │ │ ├── 06-user-a-creates-a-comment-on-the-article.bru
│ │ │ ├── 07-user-b-tries-to-delete-a-s-comment-403.bru
│ │ │ ├── 08-verify-comment-survived-the-failed-delete.bru
│ │ │ └── 09-cleanup-user-a-deletes-article.bru
│ │ ├── errors-comments/
│ │ │ ├── 01-post-comment-no-auth.bru
│ │ │ ├── 02-delete-comment-no-auth.bru
│ │ │ ├── 03-setup-register-create-article.bru
│ │ │ ├── 04-post-articles.bru
│ │ │ ├── 05-post-comment-empty-body.bru
│ │ │ ├── 06-post-comment-on-unknown-article.bru
│ │ │ ├── 07-get-comments-on-unknown-article.bru
│ │ │ ├── 08-delete-comment-on-unknown-article.bru
│ │ │ ├── 09-delete-non-existent-comment-on-existing-article.bru
│ │ │ └── 10-cleanup.bru
│ │ ├── errors-profiles/
│ │ │ ├── 01-get-unknown-profile.bru
│ │ │ ├── 02-follow-no-auth.bru
│ │ │ ├── 03-unfollow-no-auth.bru
│ │ │ ├── 04-setup-register-for-authenticated-404-tests.bru
│ │ │ ├── 05-follow-unknown-user-authed.bru
│ │ │ └── 06-unfollow-unknown-user-authed.bru
│ │ ├── favorites/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-setup-create-article.bru
│ │ │ ├── 03-favorite-article.bru
│ │ │ ├── 04-verify-favorite-persists.bru
│ │ │ ├── 05-articles-filtered-by-favorited-username.bru
│ │ │ ├── 06-articles-filtered-by-favorited-username-with-auth.bru
│ │ │ ├── 07-unfavorite-article.bru
│ │ │ ├── 08-verify-unfavorite-persists.bru
│ │ │ └── 09-cleanup.bru
│ │ ├── feed/
│ │ │ ├── 01-register-main-user.bru
│ │ │ ├── 02-register-celeb-user.bru
│ │ │ ├── 03-feed-for-new-user-returns-empty.bru
│ │ │ ├── 04-main-follows-celeb.bru
│ │ │ ├── 05-celeb-creates-article-1.bru
│ │ │ ├── 06-celeb-creates-article-2.bru
│ │ │ ├── 07-main-checks-feed.bru
│ │ │ ├── 08-feed-with-limit-1.bru
│ │ │ ├── 09-feed-with-limit-1-offset-1.bru
│ │ │ ├── 10-cleanup-delete-articles.bru
│ │ │ ├── 11-delete-slug2.bru
│ │ │ └── 12-cleanup-unfollow.bru
│ │ ├── pagination/
│ │ │ ├── 01-setup-register.bru
│ │ │ ├── 02-create-article-1.bru
│ │ │ ├── 03-create-article-2.bru
│ │ │ ├── 04-list-with-limit-1-most-recent-first-so-slug2.bru
│ │ │ ├── 05-list-with-limit-1-offset-1-second-page-so-slug1.bru
│ │ │ ├── 06-cleanup.bru
│ │ │ └── 07-delete-slug2.bru
│ │ ├── profiles/
│ │ │ ├── 01-register-main-user.bru
│ │ │ ├── 02-register-celeb-user.bru
│ │ │ ├── 03-get-profile-without-auth.bru
│ │ │ ├── 04-get-profile-with-auth.bru
│ │ │ ├── 05-follow-profile.bru
│ │ │ ├── 06-unfollow-profile.bru
│ │ │ └── 07-verify-unfollow-persisted.bru
│ │ └── tags/
│ │ ├── 01-setup-register.bru
│ │ ├── 02-setup-create-article-with-tags.bru
│ │ ├── 03-get-tags.bru
│ │ └── 04-cleanup.bru
│ ├── hurl/
│ │ ├── articles.hurl
│ │ ├── auth.hurl
│ │ ├── comments.hurl
│ │ ├── errors_articles.hurl
│ │ ├── errors_auth.hurl
│ │ ├── errors_authorization.hurl
│ │ ├── errors_comments.hurl
│ │ ├── errors_profiles.hurl
│ │ ├── favorites.hurl
│ │ ├── feed.hurl
│ │ ├── pagination.hurl
│ │ ├── profiles.hurl
│ │ ├── run-hurl-tests.sh
│ │ └── tags.hurl
│ ├── hurl-to-bruno.js
│ ├── openapi.yml
│ ├── run-api-tests-bruno.sh
│ └── run-api-tests-hurl.sh
└── e2e/
├── SELECTORS.md
├── articles.spec.ts
├── auth.spec.ts
├── comments.spec.ts
├── error-handling.spec.ts
├── health.spec.ts
├── helpers/
│ ├── api.ts
│ ├── articles.ts
│ ├── auth.ts
│ ├── comments.ts
│ ├── config.ts
│ ├── debug.ts
│ ├── profile.ts
│ └── setup.ts
├── navigation.spec.ts
├── null-fields.spec.ts
├── playwright.base.ts
├── settings.spec.ts
├── social.spec.ts
├── url-navigation.spec.ts
├── user-fetch-errors.spec.ts
└── xss-security.spec.ts
SYMBOL INDEX (63 symbols across 14 files)
FILE: assets/media/conduit-logo.svg.generate.ts
constant FONT_URL (line 6) | const FONT_URL = 'https://fonts.gstatic.com/s/caudex/v19/esDT311QOP6BJUr...
constant TMP_DIR (line 7) | const TMP_DIR = '.tmp';
constant FONT_PATH (line 8) | const FONT_PATH = `${TMP_DIR}/caudex-bold.ttf`;
FILE: docs/astro.config.mjs
function removeMdExtension (line 42) | function removeMdExtension() {
FILE: specs/api/hurl-to-bruno.js
constant ROOT (line 9) | const ROOT = resolve(import.meta.dirname);
constant HURL_DIR (line 10) | const HURL_DIR = join(ROOT, "hurl");
constant BRUNO_DIR (line 11) | const BRUNO_DIR = join(ROOT, "bruno");
constant CHECK_MODE (line 12) | const CHECK_MODE = process.argv.includes("--check");
function parseHurlFile (line 16) | function parseHurlFile(filePath) {
function slugify (line 177) | function slugify(str) {
function folderName (line 184) | function folderName(filename) {
function fileName (line 188) | function fileName(request, index, method, url) {
function jsonpathToJs (line 201) | function jsonpathToJs(jp) {
function jsonpathToPropertyCheck (line 210) | function jsonpathToPropertyCheck(jp) {
function transformValue (line 227) | function transformValue(rawValue) {
function assertToJs (line 284) | function assertToJs(assertLine) {
function generateBruFile (line 405) | function generateBruFile(request, seq) {
function generateCollection (line 467) | function generateCollection(outputDir) {
function collectFiles (line 529) | function collectFiles(dir, prefix = "") {
function checkMode (line 545) | function checkMode() {
FILE: specs/e2e/error-handling.spec.ts
constant API_BASE (line 8) | const API_BASE = 'https://api.realworld.show/api';
function mockApiError (line 13) | async function mockApiError(page: Page, endpoint: string, status: number...
function setFakeAuthToken (line 29) | async function setFakeAuthToken(page: Page) {
FILE: specs/e2e/helpers/api.ts
type UserCredentials (line 4) | interface UserCredentials {
function registerUserViaAPI (line 10) | async function registerUserViaAPI(request: APIRequestContext, user: User...
function loginUserViaAPI (line 27) | async function loginUserViaAPI(request: APIRequestContext, email: string...
function createArticleViaAPI (line 43) | async function createArticleViaAPI(
function updateUserViaAPI (line 68) | async function updateUserViaAPI(
function createManyArticles (line 86) | async function createManyArticles(
FILE: specs/e2e/helpers/articles.ts
type ArticleData (line 3) | interface ArticleData {
function createArticle (line 10) | async function createArticle(page: Page, article: ArticleData, options: ...
function editArticle (line 35) | async function editArticle(page: Page, slug: string, updates: Partial<Ar...
function deleteArticle (line 58) | async function deleteArticle(page: Page) {
function favoriteArticle (line 63) | async function favoriteArticle(page: Page) {
function unfavoriteArticle (line 69) | async function unfavoriteArticle(page: Page) {
function generateUniqueArticle (line 75) | function generateUniqueArticle(): ArticleData {
FILE: specs/e2e/helpers/auth.ts
function register (line 3) | async function register(page: Page, username: string, email: string, pas...
function login (line 25) | async function login(page: Page, email: string, password: string) {
function logout (line 46) | async function logout(page: Page) {
function generateUniqueUser (line 51) | function generateUniqueUser() {
FILE: specs/e2e/helpers/comments.ts
function addComment (line 3) | async function addComment(page: Page, commentText: string) {
function deleteComment (line 21) | async function deleteComment(page: Page, commentText: string) {
function getCommentCount (line 29) | async function getCommentCount(page: Page): Promise<number> {
FILE: specs/e2e/helpers/config.ts
constant API_MODE (line 1) | const API_MODE = process.env.API_MODE?.toLowerCase() !== 'false';
constant API_BASE (line 2) | const API_BASE = process.env.API_BASE || 'https://api.realworld.show/api';
FILE: specs/e2e/helpers/debug.ts
type AuthState (line 22) | type AuthState = 'authenticated' | 'unauthenticated' | 'unavailable' | '...
type User (line 24) | interface User {
function getToken (line 36) | async function getToken(page: Page): Promise<string | null> {
function getAuthState (line 44) | async function getAuthState(page: Page): Promise<AuthState | undefined> {
function getCurrentUser (line 52) | async function getCurrentUser(page: Page): Promise<User | null> {
function waitForAuthState (line 60) | async function waitForAuthState(
function isDebugInterfaceAvailable (line 73) | async function isDebugInterfaceAvailable(page: Page): Promise<boolean> {
FILE: specs/e2e/helpers/profile.ts
function followUser (line 4) | async function followUser(page: Page, username: string) {
function unfollowUser (line 13) | async function unfollowUser(page: Page, username: string) {
function updateProfile (line 22) | async function updateProfile(
FILE: specs/e2e/helpers/setup.ts
type UserCredentials (line 6) | interface UserCredentials {
function createUserInIsolation (line 16) | async function createUserInIsolation(
function createManyArticles (line 32) | async function createManyArticles(
FILE: specs/e2e/user-fetch-errors.spec.ts
constant API_BASE (line 9) | const API_BASE = 'https://api.realworld.show/api';
FILE: specs/e2e/xss-security.spec.ts
constant XSS_IMAGE_PAYLOADS (line 43) | const XSS_IMAGE_PAYLOADS = [
constant XSS_MARKDOWN_PAYLOADS (line 67) | const XSS_MARKDOWN_PAYLOADS = [
function setupXssDetector (line 98) | function setupXssDetector(page: Page): () => boolean {
function injectToken (line 110) | async function injectToken(page: Page, token: string): Promise<void> {
Condensed preview — 245 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (469K chars).
[
{
"path": ".github/CODEOWNERS",
"chars": 1,
"preview": "\n"
},
{
"path": ".github/ISSUE_TEMPLATE/BUG_REPORT.yml",
"chars": 710,
"preview": "name: 🐞 Bug report\ndescription: Report a bug in the RealWorld project\ntitle: '[Bug]: '\nlabels:\n - bug\nbody:\n - type: d"
},
{
"path": ".github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml",
"chars": 1045,
"preview": "name: 🚀 Feature request\ndescription: Suggest a feature for RealWorld project\ntitle: '[Feature Request]:'\nbody:\n - type:"
},
{
"path": ".github/dependabot.yml",
"chars": 264,
"preview": "version: 2\nupdates:\n - package-ecosystem: github-actions\n directory: '/'\n schedule:\n interval: weekly\n op"
},
{
"path": ".github/workflows/bruno-check.yml",
"chars": 368,
"preview": "name: 'Bruno Check'\n\non:\n push:\n pull_request:\n\njobs:\n bruno-check:\n name: Verify Bruno collection is up-to-date\n "
},
{
"path": ".github/workflows/codeql.yml",
"chars": 679,
"preview": "name: 'CodeQL'\n\non:\n workflow_dispatch:\n schedule:\n - cron: '24 3 * * 3'\n\njobs:\n analyze:\n name: Analyze\n ru"
},
{
"path": ".github/workflows/deploy-docs.yml",
"chars": 1154,
"preview": "name: Deploy Documentation\n\non:\n push:\n branches: [main]\n paths:\n - 'docs/**'\n - '.github/workflows/dep"
},
{
"path": ".github/workflows/spammy-guardian.yml",
"chars": 481,
"preview": "name: Spammy Guardian\non:\n workflow_dispatch:\n inputs:\n issueId:\n description: 'id of the issue to test "
},
{
"path": ".gitignore",
"chars": 482,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# Dependencies\nnode_modules\n.pnp\n"
},
{
"path": "CONTRIBUTING.md",
"chars": 7118,
"preview": "# Contributing to RealWorld\n\nWe would love for you to contribute to RealWorld and help make it even better than it is\nto"
},
{
"path": "LICENSE",
"chars": 1284,
"preview": "MIT License\n\nCopyright (c) 2021 Thinkster\nCopyright (c) 2026 c4ffein\n\nPermission is hereby granted, free of charge, to a"
},
{
"path": "Makefile",
"chars": 999,
"preview": ".PHONY: help \\\n\tbruno-generate \\\n\tbruno-check \\\n\tdocumentation-setup \\\n\tdocumentation-dev \\\n\tdocumentation-dev-host \\\n\td"
},
{
"path": "README.md",
"chars": 3191,
"preview": "\n\n<p align=\"center\" style=\"margin-top: 30px;\">\n "
},
{
"path": "assets/media/conduit-logo.svg.generate.ts",
"chars": 1174,
"preview": "import opentype from 'opentype.js';\nimport { mkdirSync } from 'fs';\n\nprocess.chdir(import.meta.dir);\n\nconst FONT_URL = '"
},
{
"path": "assets/media/mobile_icons/ios/AppIcon.appiconset/Contents.json",
"chars": 3540,
"preview": "{\n \"images\": [\n {\n \"idiom\": \"iphone\",\n \"size\": \"20x20\",\n \"scale\": \"2x\",\n \"filename\": \"Icon-App-2"
},
{
"path": "assets/media/mobile_icons/ios/README.md",
"chars": 999,
"preview": "## iTunesArtwork & iTunesArtwork@2x (App Icon) file extension:\n\nPNG extension is prepended to these two files -\n\nWhile A"
},
{
"path": "assets/media/mobile_icons/watchkit/AppIcon.appiconset/Contents.json",
"chars": 1419,
"preview": "{\n \"images\": [\n {\n \"size\": \"24x24\",\n \"idiom\": \"watch\",\n \"scale\": \"2x\",\n \"filename\": \"Icon-24@2x."
},
{
"path": "assets/theme/styles.css",
"chars": 28850,
"preview": "/*\n * ============================================================================\n * Conduit Minimal CSS v4\n * Only inc"
},
{
"path": "docs/.gitignore",
"chars": 229,
"preview": "# build output\ndist/\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn"
},
{
"path": "docs/.vscode/extensions.json",
"chars": 87,
"preview": "{\n \"recommendations\": [\"astro-build.astro-vscode\"],\n \"unwantedRecommendations\": []\n}\n"
},
{
"path": "docs/.vscode/launch.json",
"chars": 207,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"command\": \"./node_modules/.bin/astro dev\",\n \"name\": \"Dev"
},
{
"path": "docs/README.md",
"chars": 2564,
"preview": "# Starlight Starter Kit: Basics\n\n[](https"
},
{
"path": "docs/astro.config.mjs",
"chars": 6784,
"preview": "import {defineConfig} from 'astro/config';\nimport starlight from '@astrojs/starlight';\nimport tailwindcss from \"@tailwin"
},
{
"path": "docs/non-included/LICENSES_LOGOS.md",
"chars": 5610,
"preview": "# Logo Licenses & Trademark Analysis\n\n> **Disclaimer**: This document was generated by Claude (Anthropic) and is provide"
},
{
"path": "docs/package.json",
"chars": 669,
"preview": "{\n \"name\": \"documentation\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"scripts\": {\n \"dev\": \"astro dev\",\n \"start"
},
{
"path": "docs/src/content/config.ts",
"chars": 190,
"preview": "import { defineCollection } from 'astro:content';\nimport { docsSchema } from '@astrojs/starlight/schema';\n\nexport const "
},
{
"path": "docs/src/content/docs/community/authors.md",
"chars": 3386,
"preview": "---\ntitle: Authors\n---\n\n# Who currently maintains the project\n\n#### [Gérôme Grignon](https://github.com/geromegrignon) -"
},
{
"path": "docs/src/content/docs/community/resources.md",
"chars": 1041,
"preview": "---\ntitle: Resources\n---\n\n# Community created resources\n\n- Performance comparisons:\n - [A Real-World Comparison of Fron"
},
{
"path": "docs/src/content/docs/community/special-thanks.md",
"chars": 1255,
"preview": "---\ntitle: Special thanks\n---\n\nRealWorld would not be possible without the open source community's assistance in reviewi"
},
{
"path": "docs/src/content/docs/implementation-creation/expectations.md",
"chars": 2804,
"preview": "---\ntitle: Expectations\n---\n\n## Remember: Keep your codebases _simple_, yet _robust_.\n\nIf a new developer to your framew"
},
{
"path": "docs/src/content/docs/implementation-creation/features.md",
"chars": 352,
"preview": "---\ntitle: Features\n---\n\n**General functionality:**\n\n- Authenticate users via JWT (login/signup pages + logout button on"
},
{
"path": "docs/src/content/docs/implementation-creation/introduction.md",
"chars": 757,
"preview": "---\ntitle: Introduction\n---\n\n**Conduit** is a social blogging site (i.e. a Medium.com clone). It uses a custom API for a"
},
{
"path": "docs/src/content/docs/index.mdx",
"chars": 612,
"preview": "---\ntitle: RealWorld apps\ndescription: It's all about building real world, production ready apps.\ntemplate: splash\nhero:"
},
{
"path": "docs/src/content/docs/introduction.mdx",
"chars": 2098,
"preview": "---\ntitle: Introduction\n---\n\n# Introduction\n\n> See how _the exact same_ Medium.com clone is built using different [front"
},
{
"path": "docs/src/content/docs/specifications/backend/api-response-format.md",
"chars": 3148,
"preview": "---\ntitle: API response format\n---\n\n## JSON Objects returned by API:\n\nMake sure the right content type like `Content-Typ"
},
{
"path": "docs/src/content/docs/specifications/backend/bruno.md",
"chars": 812,
"preview": "---\ntitle: Bruno\n---\n\nFor your convenience, we have a [Bruno collection](https://github.com/realworld-apps/realworld/tre"
},
{
"path": "docs/src/content/docs/specifications/backend/cors.md",
"chars": 346,
"preview": "---\ntitle: CORS\n---\n\n## Considerations for your backend with [CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_"
},
{
"path": "docs/src/content/docs/specifications/backend/endpoints.md",
"chars": 5048,
"preview": "---\ntitle: Endpoints\n---\n\n### Authentication Header:\n\nYou can read the authentication header from the headers of the req"
},
{
"path": "docs/src/content/docs/specifications/backend/error-handling.md",
"chars": 541,
"preview": "---\ntitle: Error handling\n---\n\n### Errors and Status Codes\n\nIf a request fails any validations, expect a 422 and errors "
},
{
"path": "docs/src/content/docs/specifications/backend/hurl.md",
"chars": 645,
"preview": "---\ntitle: Hurl\n---\n\nFor your convenience, we have a [Hurl collection](https://github.com/realworld-apps/realworld/tree/"
},
{
"path": "docs/src/content/docs/specifications/backend/introduction.md",
"chars": 852,
"preview": "---\ntitle: Introduction\n---\n\nAll backend implementations need to adhere to our [API spec](https://github.com/realworld-a"
},
{
"path": "docs/src/content/docs/specifications/backend/postman.md",
"chars": 441,
"preview": "---\ntitle: Postman\n---\n\nFor your convenience, we have a [Postman collection](https://github.com/realworld-apps/realworld"
},
{
"path": "docs/src/content/docs/specifications/backend/tests.md",
"chars": 148,
"preview": "---\ntitle: Tests\n---\n\nInclude _at least_ **one** unit test in your repo to demonstrate how testing works (full testing c"
},
{
"path": "docs/src/content/docs/specifications/frontend/api.md",
"chars": 1294,
"preview": "---\ntitle: API\n---\n\nThis project provides you different solutions to test your frontend implementation with an API by:\n\n"
},
{
"path": "docs/src/content/docs/specifications/frontend/routing.md",
"chars": 900,
"preview": "---\ntitle: Routing\n---\n\n- Home page (URL: `/` )\n - List of tags\n - List of articles pulled from either Feed, Global, o"
},
{
"path": "docs/src/content/docs/specifications/frontend/styles.md",
"chars": 733,
"preview": "---\ntitle: Styles\n---\n\nAll frontend implementations should use the shared [styles.css](https://github.com/realworld-apps"
},
{
"path": "docs/src/content/docs/specifications/frontend/templates.md",
"chars": 19532,
"preview": "---\ntitle: Templates\n---\n\n## Head\n\nThe `<head>` element includes all the metadata for a page, including the title, descr"
},
{
"path": "docs/src/content/docs/specifications/frontend/tests.md",
"chars": 1330,
"preview": "---\ntitle: Tests\n---\n\nInclude _at least_ **one** unit test in your repo to demonstrate how testing works (full testing c"
},
{
"path": "docs/src/content/docs/specifications/mobile-specs/introduction.md",
"chars": 690,
"preview": "---\ntitle: Introduction\n---\n\n### [Icons for (iOS/Android)](https://github.com/realworld-apps/realworld/tree/master/spec/"
},
{
"path": "docs/src/env.d.ts",
"chars": 85,
"preview": "/// <reference path=\"../.astro/types.d.ts\" />\n/// <reference types=\"astro/client\" />\n"
},
{
"path": "docs/src/tailwind.css",
"chars": 942,
"preview": "@import \"tailwindcss\";\n@import \"@astrojs/starlight-tailwind\";\n@source \"../**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,v"
},
{
"path": "docs/tsconfig.json",
"chars": 124,
"preview": "{\n \"extends\": \"astro/tsconfigs/strict\",\n \"compilerOptions\": {\n \"jsx\": \"react-jsx\",\n \"jsxImportSource\": \"react\"\n "
},
{
"path": "specs/api/README.md",
"chars": 851,
"preview": "# RealWorld API Spec\n\n## Running API tests locally\n\n### With Hurl\n\nTo locally run the provided [Hurl](https://hurl.dev) "
},
{
"path": "specs/api/bruno/articles/01-setup-register.bru",
"chars": 367,
"preview": "meta {\n name: Setup: Register\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbo"
},
{
"path": "specs/api/bruno/articles/02-create-article-with-tags.bru",
"chars": 1479,
"preview": "meta {\n name: Create article with tags\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth"
},
{
"path": "specs/api/bruno/articles/03-list-all-articles.bru",
"chars": 1085,
"preview": "meta {\n name: List all articles\n type: http\n seq: 3\n}\n\nget {\n url: {{host}}/api/articles\n body: none\n auth: none\n}"
},
{
"path": "specs/api/bruno/articles/04-list-by-author.bru",
"chars": 1112,
"preview": "meta {\n name: List by author\n type: http\n seq: 4\n}\n\nget {\n url: {{host}}/api/articles?author=art_{{uid}}\n body: non"
},
{
"path": "specs/api/bruno/articles/05-list-all-articles-with-auth.bru",
"chars": 1141,
"preview": "meta {\n name: List all articles with auth\n type: http\n seq: 5\n}\n\nget {\n url: {{host}}/api/articles\n body: none\n au"
},
{
"path": "specs/api/bruno/articles/06-list-by-author-with-auth.bru",
"chars": 1168,
"preview": "meta {\n name: List by author with auth\n type: http\n seq: 6\n}\n\nget {\n url: {{host}}/api/articles?author=art_{{uid}}\n "
},
{
"path": "specs/api/bruno/articles/07-list-by-tag.bru",
"chars": 1170,
"preview": "meta {\n name: List by tag\n type: http\n seq: 7\n}\n\nget {\n url: {{host}}/api/articles?tag=d_{{uid}}\n body: none\n auth"
},
{
"path": "specs/api/bruno/articles/08-list-articles-without-auth.bru",
"chars": 311,
"preview": "meta {\n name: List articles without auth\n type: http\n seq: 8\n}\n\nget {\n url: {{host}}/api/articles\n body: none\n aut"
},
{
"path": "specs/api/bruno/articles/09-get-single-article.bru",
"chars": 890,
"preview": "meta {\n name: Get single article\n type: http\n seq: 9\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}\n body: none\n au"
},
{
"path": "specs/api/bruno/articles/10-update-article-body.bru",
"chars": 1215,
"preview": "meta {\n name: Update article body\n type: http\n seq: 10\n}\n\nput {\n url: {{host}}/api/articles/{{slug}}\n body: json\n "
},
{
"path": "specs/api/bruno/articles/11-verify-update-persisted.bru",
"chars": 1130,
"preview": "meta {\n name: Verify update persisted\n type: http\n seq: 11\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}\n body: non"
},
{
"path": "specs/api/bruno/articles/12-update-article-without-taglist-tags-should-be-preserved.bru",
"chars": 633,
"preview": "meta {\n name: Update article without tagList: tags should be preserved\n type: http\n seq: 12\n}\n\nput {\n url: {{host}}/"
},
{
"path": "specs/api/bruno/articles/13-update-article-remove-all-tags-with-empty-array.bru",
"chars": 450,
"preview": "meta {\n name: Update article: remove all tags with empty array\n type: http\n seq: 13\n}\n\nput {\n url: {{host}}/api/arti"
},
{
"path": "specs/api/bruno/articles/14-verify-tags-were-actually-removed.bru",
"chars": 323,
"preview": "meta {\n name: Verify tags were actually removed\n type: http\n seq: 14\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}\n "
},
{
"path": "specs/api/bruno/articles/15-update-article-taglist-null-should-be-rejected.bru",
"chars": 308,
"preview": "meta {\n name: Update article: tagList null should be rejected\n type: http\n seq: 15\n}\n\nput {\n url: {{host}}/api/artic"
},
{
"path": "specs/api/bruno/articles/16-delete-article.bru",
"chars": 210,
"preview": "meta {\n name: Delete article\n type: http\n seq: 16\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}\n body: none\n au"
},
{
"path": "specs/api/bruno/articles/17-verify-deletion.bru",
"chars": 246,
"preview": "meta {\n name: Verify deletion\n type: http\n seq: 17\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}\n body: none\n auth"
},
{
"path": "specs/api/bruno/auth/01-register.bru",
"chars": 700,
"preview": "meta {\n name: Register\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbody:json"
},
{
"path": "specs/api/bruno/auth/02-login.bru",
"chars": 665,
"preview": "meta {\n name: Login\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/users/login\n body: json\n auth: none\n}\n\nbody:j"
},
{
"path": "specs/api/bruno/auth/03-get-current-user.bru",
"chars": 555,
"preview": "meta {\n name: Get current user\n type: http\n seq: 3\n}\n\nget {\n url: {{host}}/api/user\n body: none\n auth: none\n}\n\nhea"
},
{
"path": "specs/api/bruno/auth/04-update-user.bru",
"chars": 631,
"preview": "meta {\n name: Update user\n type: http\n seq: 4\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth: none\n}\n\nheaders "
},
{
"path": "specs/api/bruno/auth/05-verify-update-persisted.bru",
"chars": 573,
"preview": "meta {\n name: Verify update persisted\n type: http\n seq: 5\n}\n\nget {\n url: {{host}}/api/user\n body: none\n auth: none"
},
{
"path": "specs/api/bruno/auth/06-update-user-bio-to-empty-string-should-normalize-to-null.bru",
"chars": 362,
"preview": "meta {\n name: Update user bio to empty string - should normalize to null\n type: http\n seq: 6\n}\n\nput {\n url: {{host}}"
},
{
"path": "specs/api/bruno/auth/07-verify-empty-string-normalization-persisted.bru",
"chars": 288,
"preview": "meta {\n name: Verify empty string normalization persisted\n type: http\n seq: 7\n}\n\nget {\n url: {{host}}/api/user\n bod"
},
{
"path": "specs/api/bruno/auth/08-restore-bio-then-set-to-null.bru",
"chars": 358,
"preview": "meta {\n name: Restore bio then set to null\n type: http\n seq: 8\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth:"
},
{
"path": "specs/api/bruno/auth/09-update-user-bio-to-null-should-accept-for-nullable-field.bru",
"chars": 364,
"preview": "meta {\n name: Update user bio to null - should accept for nullable field\n type: http\n seq: 9\n}\n\nput {\n url: {{host}}"
},
{
"path": "specs/api/bruno/auth/10-verify-null-bio-persisted.bru",
"chars": 271,
"preview": "meta {\n name: Verify null bio persisted\n type: http\n seq: 10\n}\n\nget {\n url: {{host}}/api/user\n body: none\n auth: n"
},
{
"path": "specs/api/bruno/auth/11-restore-bio.bru",
"chars": 632,
"preview": "meta {\n name: Restore bio\n type: http\n seq: 11\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth: none\n}\n\nheaders"
},
{
"path": "specs/api/bruno/auth/12-update-user-image.bru",
"chars": 384,
"preview": "meta {\n name: Update user image\n type: http\n seq: 12\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth: none\n}\n\nh"
},
{
"path": "specs/api/bruno/auth/13-verify-image-update-persisted.bru",
"chars": 306,
"preview": "meta {\n name: Verify image update persisted\n type: http\n seq: 13\n}\n\nget {\n url: {{host}}/api/user\n body: none\n aut"
},
{
"path": "specs/api/bruno/auth/14-update-image-to-empty-string-should-normalize-to-null.bru",
"chars": 364,
"preview": "meta {\n name: Update image to empty string - should normalize to null\n type: http\n seq: 14\n}\n\nput {\n url: {{host}}/a"
},
{
"path": "specs/api/bruno/auth/15-verify-image-empty-string-normalization-persisted.bru",
"chars": 297,
"preview": "meta {\n name: Verify image empty string normalization persisted\n type: http\n seq: 15\n}\n\nget {\n url: {{host}}/api/use"
},
{
"path": "specs/api/bruno/auth/16-set-image-then-update-to-null-should-accept-for-nullable-field.bru",
"chars": 429,
"preview": "meta {\n name: Set image then update to null - should accept for nullable field\n type: http\n seq: 16\n}\n\nput {\n url: {"
},
{
"path": "specs/api/bruno/auth/17-put-user.bru",
"chars": 319,
"preview": "meta {\n name: PUT user\n type: http\n seq: 17\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth: none\n}\n\nheaders {\n"
},
{
"path": "specs/api/bruno/auth/18-verify-null-image-persisted.bru",
"chars": 275,
"preview": "meta {\n name: Verify null image persisted\n type: http\n seq: 18\n}\n\nget {\n url: {{host}}/api/user\n body: none\n auth:"
},
{
"path": "specs/api/bruno/auth/19-update-username-and-email.bru",
"chars": 765,
"preview": "meta {\n name: Update username and email\n type: http\n seq: 19\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth: n"
},
{
"path": "specs/api/bruno/auth/20-verify-username-email-update-persisted.bru",
"chars": 610,
"preview": "meta {\n name: Verify username/email update persisted\n type: http\n seq: 20\n}\n\nget {\n url: {{host}}/api/user\n body: n"
},
{
"path": "specs/api/bruno/bruno.json",
"chars": 72,
"preview": "{\n \"version\": \"1\",\n \"name\": \"RealWorld API\",\n \"type\": \"collection\"\n}\n"
},
{
"path": "specs/api/bruno/collection.bru",
"chars": 146,
"preview": "script:pre-request {\n if (!bru.getVar(\"uid\")) {\n bru.setVar(\"uid\", Date.now().toString() + Math.random().toString(36"
},
{
"path": "specs/api/bruno/comments/01-setup-register.bru",
"chars": 367,
"preview": "meta {\n name: Setup: Register\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbo"
},
{
"path": "specs/api/bruno/comments/02-setup-create-article.bru",
"chars": 430,
"preview": "meta {\n name: Setup: Create article\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: n"
},
{
"path": "specs/api/bruno/comments/03-create-comment.bru",
"chars": 759,
"preview": "meta {\n name: Create comment\n type: http\n seq: 3\n}\n\npost {\n url: {{host}}/api/articles/{{slug}}/comments\n body: jso"
},
{
"path": "specs/api/bruno/comments/04-list-comments.bru",
"chars": 753,
"preview": "meta {\n name: List comments\n type: http\n seq: 4\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}/comments\n body: none\n"
},
{
"path": "specs/api/bruno/comments/05-list-comments-without-auth.bru",
"chars": 718,
"preview": "meta {\n name: List comments without auth\n type: http\n seq: 5\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}/comments\n"
},
{
"path": "specs/api/bruno/comments/06-delete-comment.bru",
"chars": 233,
"preview": "meta {\n name: Delete comment\n type: http\n seq: 6\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}/comments/{{comment"
},
{
"path": "specs/api/bruno/comments/07-verify-deletion.bru",
"chars": 242,
"preview": "meta {\n name: Verify deletion\n type: http\n seq: 7\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}/comments\n body: non"
},
{
"path": "specs/api/bruno/comments/08-selective-deletion-create-two-comments-delete-one-verify-the-other-remains.bru",
"chars": 436,
"preview": "meta {\n name: Selective deletion: create two comments, delete one, verify the other remains\n type: http\n seq: 8\n}\n\npo"
},
{
"path": "specs/api/bruno/comments/09-post-comments.bru",
"chars": 292,
"preview": "meta {\n name: POST comments\n type: http\n seq: 9\n}\n\npost {\n url: {{host}}/api/articles/{{slug}}/comments\n body: json"
},
{
"path": "specs/api/bruno/comments/10-verify-two-comments-exist.bru",
"chars": 253,
"preview": "meta {\n name: Verify two comments exist\n type: http\n seq: 10\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}/comments\n"
},
{
"path": "specs/api/bruno/comments/11-delete-the-first-comment.bru",
"chars": 250,
"preview": "meta {\n name: Delete the first comment\n type: http\n seq: 11\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}/comment"
},
{
"path": "specs/api/bruno/comments/12-verify-only-the-second-comment-remains.bru",
"chars": 328,
"preview": "meta {\n name: Verify only the second comment remains\n type: http\n seq: 12\n}\n\nget {\n url: {{host}}/api/articles/{{slu"
},
{
"path": "specs/api/bruno/comments/13-cleanup.bru",
"chars": 203,
"preview": "meta {\n name: Cleanup\n type: http\n seq: 13\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}\n body: none\n auth: non"
},
{
"path": "specs/api/bruno/environments/local.bru",
"chars": 39,
"preview": "vars {\n host: http://localhost:3000\n}\n"
},
{
"path": "specs/api/bruno/errors-articles/01-create-article-no-auth.bru",
"chars": 373,
"preview": "meta {\n name: Create article no auth\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: "
},
{
"path": "specs/api/bruno/errors-articles/02-get-unknown-slug.bru",
"chars": 258,
"preview": "meta {\n name: GET unknown slug\n type: http\n seq: 2\n}\n\nget {\n url: {{host}}/api/articles/unknown-slug-{{uid}}\n body:"
},
{
"path": "specs/api/bruno/errors-articles/03-update-no-auth.bru",
"chars": 311,
"preview": "meta {\n name: Update no auth\n type: http\n seq: 3\n}\n\nput {\n url: {{host}}/api/articles/some-slug\n body: json\n auth:"
},
{
"path": "specs/api/bruno/errors-articles/04-delete-no-auth.bru",
"chars": 247,
"preview": "meta {\n name: Delete no auth\n type: http\n seq: 4\n}\n\ndelete {\n url: {{host}}/api/articles/some-slug\n body: none\n au"
},
{
"path": "specs/api/bruno/errors-articles/05-get-feed-no-auth.bru",
"chars": 241,
"preview": "meta {\n name: GET feed no auth\n type: http\n seq: 5\n}\n\nget {\n url: {{host}}/api/articles/feed\n body: none\n auth: no"
},
{
"path": "specs/api/bruno/errors-articles/06-favorite-no-auth.bru",
"chars": 256,
"preview": "meta {\n name: Favorite no auth\n type: http\n seq: 6\n}\n\npost {\n url: {{host}}/api/articles/some-slug/favorite\n body: "
},
{
"path": "specs/api/bruno/errors-articles/07-unfavorite-no-auth.bru",
"chars": 260,
"preview": "meta {\n name: Unfavorite no auth\n type: http\n seq: 7\n}\n\ndelete {\n url: {{host}}/api/articles/some-slug/favorite\n bo"
},
{
"path": "specs/api/bruno/errors-articles/08-setup-register-for-authenticated-error-tests.bru",
"chars": 403,
"preview": "meta {\n name: Setup: Register for authenticated error tests\n type: http\n seq: 8\n}\n\npost {\n url: {{host}}/api/users\n "
},
{
"path": "specs/api/bruno/errors-articles/09-create-article-empty-title.bru",
"chars": 412,
"preview": "meta {\n name: Create article empty title\n type: http\n seq: 9\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n au"
},
{
"path": "specs/api/bruno/errors-articles/10-create-article-empty-description.bru",
"chars": 437,
"preview": "meta {\n name: Create article empty description\n type: http\n seq: 10\n}\n\npost {\n url: {{host}}/api/articles\n body: js"
},
{
"path": "specs/api/bruno/errors-articles/11-create-article-empty-body.bru",
"chars": 423,
"preview": "meta {\n name: Create article empty body\n type: http\n seq: 11\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n au"
},
{
"path": "specs/api/bruno/errors-articles/12-duplicate-titles-are-allowed-each-gets-a-unique-slug.bru",
"chars": 445,
"preview": "meta {\n name: Duplicate titles are allowed (each gets a unique slug)\n type: http\n seq: 12\n}\n\npost {\n url: {{host}}/a"
},
{
"path": "specs/api/bruno/errors-articles/13-post-articles.bru",
"chars": 471,
"preview": "meta {\n name: POST articles\n type: http\n seq: 13\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: none\n}\n\n"
},
{
"path": "specs/api/bruno/errors-articles/14-update-unknown-slug.bru",
"chars": 375,
"preview": "meta {\n name: Update unknown slug\n type: http\n seq: 14\n}\n\nput {\n url: {{host}}/api/articles/unknown-slug-{{uid}}\n b"
},
{
"path": "specs/api/bruno/errors-articles/15-favorite-unknown-slug.bru",
"chars": 320,
"preview": "meta {\n name: Favorite unknown slug\n type: http\n seq: 15\n}\n\npost {\n url: {{host}}/api/articles/unknown-slug-{{uid}}/"
},
{
"path": "specs/api/bruno/errors-articles/16-unfavorite-unknown-slug.bru",
"chars": 324,
"preview": "meta {\n name: Unfavorite unknown slug\n type: http\n seq: 16\n}\n\ndelete {\n url: {{host}}/api/articles/unknown-slug-{{ui"
},
{
"path": "specs/api/bruno/errors-articles/17-update-unknown-slug.bru",
"chars": 375,
"preview": "meta {\n name: Update unknown slug\n type: http\n seq: 17\n}\n\nput {\n url: {{host}}/api/articles/unknown-slug-{{uid}}\n b"
},
{
"path": "specs/api/bruno/errors-articles/18-delete-unknown-slug.bru",
"chars": 311,
"preview": "meta {\n name: Delete unknown slug\n type: http\n seq: 18\n}\n\ndelete {\n url: {{host}}/api/articles/unknown-slug-{{uid}}\n"
},
{
"path": "specs/api/bruno/errors-articles/19-cleanup.bru",
"chars": 204,
"preview": "meta {\n name: Cleanup\n type: http\n seq: 19\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug1}}\n body: none\n auth: no"
},
{
"path": "specs/api/bruno/errors-articles/20-delete-slug2.bru",
"chars": 213,
"preview": "meta {\n name: DELETE {{slug2}}\n type: http\n seq: 20\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug2}}\n body: none\n "
},
{
"path": "specs/api/bruno/errors-auth/01-register-empty-username.bru",
"chars": 389,
"preview": "meta {\n name: Register empty username\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: no"
},
{
"path": "specs/api/bruno/errors-auth/02-register-empty-email.bru",
"chars": 374,
"preview": "meta {\n name: Register empty email\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n"
},
{
"path": "specs/api/bruno/errors-auth/03-register-empty-password.bru",
"chars": 396,
"preview": "meta {\n name: Register empty password\n type: http\n seq: 3\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: no"
},
{
"path": "specs/api/bruno/errors-auth/04-register-valid-user-for-duplicate-and-login-tests.bru",
"chars": 407,
"preview": "meta {\n name: Register valid user for duplicate and login tests\n type: http\n seq: 4\n}\n\npost {\n url: {{host}}/api/use"
},
{
"path": "specs/api/bruno/errors-auth/05-register-duplicate-username.bru",
"chars": 414,
"preview": "meta {\n name: Register duplicate username\n type: http\n seq: 5\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth"
},
{
"path": "specs/api/bruno/errors-auth/06-register-duplicate-email.bru",
"chars": 408,
"preview": "meta {\n name: Register duplicate email\n type: http\n seq: 6\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: n"
},
{
"path": "specs/api/bruno/errors-auth/07-login-empty-email.bru",
"chars": 339,
"preview": "meta {\n name: Login empty email\n type: http\n seq: 7\n}\n\npost {\n url: {{host}}/api/users/login\n body: json\n auth: no"
},
{
"path": "specs/api/bruno/errors-auth/08-login-empty-password.bru",
"chars": 357,
"preview": "meta {\n name: Login empty password\n type: http\n seq: 8\n}\n\npost {\n url: {{host}}/api/users/login\n body: json\n auth:"
},
{
"path": "specs/api/bruno/errors-auth/09-login-wrong-password.bru",
"chars": 366,
"preview": "meta {\n name: Login wrong password\n type: http\n seq: 9\n}\n\npost {\n url: {{host}}/api/users/login\n body: json\n auth:"
},
{
"path": "specs/api/bruno/errors-auth/10-get-user-no-auth.bru",
"chars": 234,
"preview": "meta {\n name: GET /user no auth\n type: http\n seq: 10\n}\n\nget {\n url: {{host}}/api/user\n body: none\n auth: none\n}\n\na"
},
{
"path": "specs/api/bruno/errors-auth/11-put-user-no-auth.bru",
"chars": 297,
"preview": "meta {\n name: PUT /user no auth\n type: http\n seq: 11\n}\n\nput {\n url: {{host}}/api/user\n body: json\n auth: none\n}\n\nb"
},
{
"path": "specs/api/bruno/errors-auth/12-update-email-to-empty-string-should-reject.bru",
"chars": 285,
"preview": "meta {\n name: Update email to empty string - should reject\n type: http\n seq: 12\n}\n\nput {\n url: {{host}}/api/user\n b"
},
{
"path": "specs/api/bruno/errors-auth/13-update-username-to-empty-string-should-reject.bru",
"chars": 291,
"preview": "meta {\n name: Update username to empty string - should reject\n type: http\n seq: 13\n}\n\nput {\n url: {{host}}/api/user\n"
},
{
"path": "specs/api/bruno/errors-auth/14-update-email-to-null-should-reject.bru",
"chars": 279,
"preview": "meta {\n name: Update email to null - should reject\n type: http\n seq: 14\n}\n\nput {\n url: {{host}}/api/user\n body: jso"
},
{
"path": "specs/api/bruno/errors-auth/15-update-username-to-null-should-reject.bru",
"chars": 285,
"preview": "meta {\n name: Update username to null - should reject\n type: http\n seq: 15\n}\n\nput {\n url: {{host}}/api/user\n body: "
},
{
"path": "specs/api/bruno/errors-authorization/01-register-user-a.bru",
"chars": 377,
"preview": "meta {\n name: Register user A\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbo"
},
{
"path": "specs/api/bruno/errors-authorization/02-register-user-b.bru",
"chars": 377,
"preview": "meta {\n name: Register user B\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbo"
},
{
"path": "specs/api/bruno/errors-authorization/03-user-a-creates-article.bru",
"chars": 415,
"preview": "meta {\n name: User A creates article\n type: http\n seq: 3\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: "
},
{
"path": "specs/api/bruno/errors-authorization/04-user-b-tries-to-delete-403.bru",
"chars": 310,
"preview": "meta {\n name: User B tries to delete -> 403\n type: http\n seq: 4\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}\n b"
},
{
"path": "specs/api/bruno/errors-authorization/05-user-b-tries-to-update-403.bru",
"chars": 378,
"preview": "meta {\n name: User B tries to update -> 403\n type: http\n seq: 5\n}\n\nput {\n url: {{host}}/api/articles/{{slug}}\n body"
},
{
"path": "specs/api/bruno/errors-authorization/06-user-a-creates-a-comment-on-the-article.bru",
"chars": 392,
"preview": "meta {\n name: User A creates a comment on the article\n type: http\n seq: 6\n}\n\npost {\n url: {{host}}/api/articles/{{sl"
},
{
"path": "specs/api/bruno/errors-authorization/07-user-b-tries-to-delete-a-s-comment-403.bru",
"chars": 346,
"preview": "meta {\n name: User B tries to delete A's comment -> 403\n type: http\n seq: 7\n}\n\ndelete {\n url: {{host}}/api/articles/"
},
{
"path": "specs/api/bruno/errors-authorization/08-verify-comment-survived-the-failed-delete.bru",
"chars": 335,
"preview": "meta {\n name: Verify comment survived the failed delete\n type: http\n seq: 8\n}\n\nget {\n url: {{host}}/api/articles/{{s"
},
{
"path": "specs/api/bruno/errors-authorization/09-cleanup-user-a-deletes-article.bru",
"chars": 228,
"preview": "meta {\n name: Cleanup: User A deletes article\n type: http\n seq: 9\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}\n "
},
{
"path": "specs/api/bruno/errors-comments/01-post-comment-no-auth.bru",
"chars": 327,
"preview": "meta {\n name: Post comment no auth\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/articles/some-slug/comments\n bo"
},
{
"path": "specs/api/bruno/errors-comments/02-delete-comment-no-auth.bru",
"chars": 266,
"preview": "meta {\n name: Delete comment no auth\n type: http\n seq: 2\n}\n\ndelete {\n url: {{host}}/api/articles/some-slug/comments/"
},
{
"path": "specs/api/bruno/errors-comments/03-setup-register-create-article.bru",
"chars": 382,
"preview": "meta {\n name: Setup: Register + create article\n type: http\n seq: 3\n}\n\npost {\n url: {{host}}/api/users\n body: json\n "
},
{
"path": "specs/api/bruno/errors-comments/04-post-articles.bru",
"chars": 406,
"preview": "meta {\n name: POST articles\n type: http\n seq: 4\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: none\n}\n\nh"
},
{
"path": "specs/api/bruno/errors-comments/05-post-comment-empty-body.bru",
"chars": 374,
"preview": "meta {\n name: Post comment empty body\n type: http\n seq: 5\n}\n\npost {\n url: {{host}}/api/articles/{{slug}}/comments\n "
},
{
"path": "specs/api/bruno/errors-comments/06-post-comment-on-unknown-article.bru",
"chars": 398,
"preview": "meta {\n name: Post comment on unknown article\n type: http\n seq: 6\n}\n\npost {\n url: {{host}}/api/articles/unknown-slug"
},
{
"path": "specs/api/bruno/errors-comments/07-get-comments-on-unknown-article.bru",
"chars": 282,
"preview": "meta {\n name: Get comments on unknown article\n type: http\n seq: 7\n}\n\nget {\n url: {{host}}/api/articles/unknown-slug-"
},
{
"path": "specs/api/bruno/errors-comments/08-delete-comment-on-unknown-article.bru",
"chars": 339,
"preview": "meta {\n name: Delete comment on unknown article\n type: http\n seq: 8\n}\n\ndelete {\n url: {{host}}/api/articles/unknown-"
},
{
"path": "specs/api/bruno/errors-comments/09-delete-non-existent-comment-on-existing-article.bru",
"chars": 341,
"preview": "meta {\n name: Delete non-existent comment on existing article\n type: http\n seq: 9\n}\n\ndelete {\n url: {{host}}/api/art"
},
{
"path": "specs/api/bruno/errors-comments/10-cleanup.bru",
"chars": 203,
"preview": "meta {\n name: Cleanup\n type: http\n seq: 10\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}\n body: none\n auth: non"
},
{
"path": "specs/api/bruno/errors-profiles/01-get-unknown-profile.bru",
"chars": 261,
"preview": "meta {\n name: GET unknown profile\n type: http\n seq: 1\n}\n\nget {\n url: {{host}}/api/profiles/unknown-user-{{uid}}\n bo"
},
{
"path": "specs/api/bruno/errors-profiles/02-follow-no-auth.bru",
"chars": 263,
"preview": "meta {\n name: Follow no auth\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/profiles/unknown-user-{{uid}}/follow\n "
},
{
"path": "specs/api/bruno/errors-profiles/03-unfollow-no-auth.bru",
"chars": 267,
"preview": "meta {\n name: Unfollow no auth\n type: http\n seq: 3\n}\n\ndelete {\n url: {{host}}/api/profiles/unknown-user-{{uid}}/foll"
},
{
"path": "specs/api/bruno/errors-profiles/04-setup-register-for-authenticated-404-tests.bru",
"chars": 393,
"preview": "meta {\n name: Setup: Register for authenticated 404 tests\n type: http\n seq: 4\n}\n\npost {\n url: {{host}}/api/users\n b"
},
{
"path": "specs/api/bruno/errors-profiles/05-follow-unknown-user-authed.bru",
"chars": 324,
"preview": "meta {\n name: Follow unknown user (authed)\n type: http\n seq: 5\n}\n\npost {\n url: {{host}}/api/profiles/unknown-user-{{"
},
{
"path": "specs/api/bruno/errors-profiles/06-unfollow-unknown-user-authed.bru",
"chars": 328,
"preview": "meta {\n name: Unfollow unknown user (authed)\n type: http\n seq: 6\n}\n\ndelete {\n url: {{host}}/api/profiles/unknown-use"
},
{
"path": "specs/api/bruno/favorites/01-setup-register.bru",
"chars": 367,
"preview": "meta {\n name: Setup: Register\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbo"
},
{
"path": "specs/api/bruno/favorites/02-setup-create-article.bru",
"chars": 432,
"preview": "meta {\n name: Setup: Create article\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: n"
},
{
"path": "specs/api/bruno/favorites/03-favorite-article.bru",
"chars": 913,
"preview": "meta {\n name: Favorite article\n type: http\n seq: 3\n}\n\npost {\n url: {{host}}/api/articles/{{slug}}/favorite\n body: n"
},
{
"path": "specs/api/bruno/favorites/04-verify-favorite-persists.bru",
"chars": 911,
"preview": "meta {\n name: Verify favorite persists\n type: http\n seq: 4\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}\n body: non"
},
{
"path": "specs/api/bruno/favorites/05-articles-filtered-by-favorited-username.bru",
"chars": 1122,
"preview": "meta {\n name: Articles filtered by favorited username\n type: http\n seq: 5\n}\n\nget {\n url: {{host}}/api/articles?favor"
},
{
"path": "specs/api/bruno/favorites/06-articles-filtered-by-favorited-username-with-auth.bru",
"chars": 1178,
"preview": "meta {\n name: Articles filtered by favorited username with auth\n type: http\n seq: 6\n}\n\nget {\n url: {{host}}/api/arti"
},
{
"path": "specs/api/bruno/favorites/07-unfavorite-article.bru",
"chars": 918,
"preview": "meta {\n name: Unfavorite article\n type: http\n seq: 7\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}/favorite\n bod"
},
{
"path": "specs/api/bruno/favorites/08-verify-unfavorite-persists.bru",
"chars": 914,
"preview": "meta {\n name: Verify unfavorite persists\n type: http\n seq: 8\n}\n\nget {\n url: {{host}}/api/articles/{{slug}}\n body: n"
},
{
"path": "specs/api/bruno/favorites/09-cleanup.bru",
"chars": 202,
"preview": "meta {\n name: Cleanup\n type: http\n seq: 9\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug}}\n body: none\n auth: none"
},
{
"path": "specs/api/bruno/feed/01-register-main-user.bru",
"chars": 379,
"preview": "meta {\n name: Register main user\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n"
},
{
"path": "specs/api/bruno/feed/02-register-celeb-user.bru",
"chars": 381,
"preview": "meta {\n name: Register celeb user\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}"
},
{
"path": "specs/api/bruno/feed/03-feed-for-new-user-returns-empty.bru",
"chars": 340,
"preview": "meta {\n name: Feed for new user returns empty\n type: http\n seq: 3\n}\n\nget {\n url: {{host}}/api/articles/feed\n body: "
},
{
"path": "specs/api/bruno/feed/04-main-follows-celeb.bru",
"chars": 305,
"preview": "meta {\n name: Main follows celeb\n type: http\n seq: 4\n}\n\npost {\n url: {{host}}/api/profiles/feedc_{{uid}}/follow\n bo"
},
{
"path": "specs/api/bruno/feed/05-celeb-creates-article-1.bru",
"chars": 436,
"preview": "meta {\n name: Celeb creates article 1\n type: http\n seq: 5\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth:"
},
{
"path": "specs/api/bruno/feed/06-celeb-creates-article-2.bru",
"chars": 436,
"preview": "meta {\n name: Celeb creates article 2\n type: http\n seq: 6\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth:"
},
{
"path": "specs/api/bruno/feed/07-main-checks-feed.bru",
"chars": 1191,
"preview": "meta {\n name: Main checks feed\n type: http\n seq: 7\n}\n\nget {\n url: {{host}}/api/articles/feed\n body: none\n auth: no"
},
{
"path": "specs/api/bruno/feed/08-feed-with-limit-1.bru",
"chars": 1140,
"preview": "meta {\n name: Feed with limit=1\n type: http\n seq: 8\n}\n\nget {\n url: {{host}}/api/articles/feed?limit=1\n body: none\n "
},
{
"path": "specs/api/bruno/feed/09-feed-with-limit-1-offset-1.bru",
"chars": 352,
"preview": "meta {\n name: Feed with limit=1&offset=1\n type: http\n seq: 9\n}\n\nget {\n url: {{host}}/api/articles/feed?limit=1&offse"
},
{
"path": "specs/api/bruno/feed/10-cleanup-delete-articles.bru",
"chars": 227,
"preview": "meta {\n name: Cleanup: delete articles\n type: http\n seq: 10\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug1}}\n body"
},
{
"path": "specs/api/bruno/feed/11-delete-slug2.bru",
"chars": 219,
"preview": "meta {\n name: DELETE {{slug2}}\n type: http\n seq: 11\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug2}}\n body: none\n "
},
{
"path": "specs/api/bruno/feed/12-cleanup-unfollow.bru",
"chars": 230,
"preview": "meta {\n name: Cleanup: unfollow\n type: http\n seq: 12\n}\n\ndelete {\n url: {{host}}/api/profiles/feedc_{{uid}}/follow\n "
},
{
"path": "specs/api/bruno/pagination/01-setup-register.bru",
"chars": 369,
"preview": "meta {\n name: Setup: Register\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n\nbo"
},
{
"path": "specs/api/bruno/pagination/02-create-article-1.bru",
"chars": 421,
"preview": "meta {\n name: Create article 1\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: none\n}"
},
{
"path": "specs/api/bruno/pagination/03-create-article-2.bru",
"chars": 556,
"preview": "meta {\n name: Create article 2\n type: http\n seq: 3\n}\n\npost {\n url: {{host}}/api/articles\n body: json\n auth: none\n}"
},
{
"path": "specs/api/bruno/pagination/04-list-with-limit-1-most-recent-first-so-slug2.bru",
"chars": 393,
"preview": "meta {\n name: List with limit=1 (most recent first, so slug2)\n type: http\n seq: 4\n}\n\nget {\n url: {{host}}/api/articl"
},
{
"path": "specs/api/bruno/pagination/05-list-with-limit-1-offset-1-second-page-so-slug1.bru",
"chars": 405,
"preview": "meta {\n name: List with limit=1&offset=1 (second page, so slug1)\n type: http\n seq: 5\n}\n\nget {\n url: {{host}}/api/art"
},
{
"path": "specs/api/bruno/pagination/06-cleanup.bru",
"chars": 203,
"preview": "meta {\n name: Cleanup\n type: http\n seq: 6\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug1}}\n body: none\n auth: non"
},
{
"path": "specs/api/bruno/pagination/07-delete-slug2.bru",
"chars": 212,
"preview": "meta {\n name: DELETE {{slug2}}\n type: http\n seq: 7\n}\n\ndelete {\n url: {{host}}/api/articles/{{slug2}}\n body: none\n "
},
{
"path": "specs/api/bruno/profiles/01-register-main-user.bru",
"chars": 372,
"preview": "meta {\n name: Register main user\n type: http\n seq: 1\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}\n"
},
{
"path": "specs/api/bruno/profiles/02-register-celeb-user.bru",
"chars": 305,
"preview": "meta {\n name: Register celeb user\n type: http\n seq: 2\n}\n\npost {\n url: {{host}}/api/users\n body: json\n auth: none\n}"
},
{
"path": "specs/api/bruno/profiles/03-get-profile-without-auth.bru",
"chars": 415,
"preview": "meta {\n name: Get profile without auth\n type: http\n seq: 3\n}\n\nget {\n url: {{host}}/api/profiles/celeb_{{uid}}\n body"
},
{
"path": "specs/api/bruno/profiles/04-get-profile-with-auth.bru",
"chars": 458,
"preview": "meta {\n name: Get profile with auth\n type: http\n seq: 4\n}\n\nget {\n url: {{host}}/api/profiles/celeb_{{uid}}\n body: n"
},
{
"path": "specs/api/bruno/profiles/05-follow-profile.bru",
"chars": 458,
"preview": "meta {\n name: Follow profile\n type: http\n seq: 5\n}\n\npost {\n url: {{host}}/api/profiles/celeb_{{uid}}/follow\n body: "
},
{
"path": "specs/api/bruno/profiles/06-unfollow-profile.bru",
"chars": 463,
"preview": "meta {\n name: Unfollow profile\n type: http\n seq: 6\n}\n\ndelete {\n url: {{host}}/api/profiles/celeb_{{uid}}/follow\n bo"
}
]
// ... and 45 more files (download for full content)
About this extraction
This page contains the full source code of the realworld-apps/realworld GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 245 files (417.4 KB), approximately 120.3k tokens, and a symbol index with 63 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.