Repository navigation
Modernize the API and web app - #1862
Merged
Merged
Conversation
Each project now has a mise.toml: api pins Ruby and Postgres, and web pins Node. These replace the .tool-versions and .ruby-version files. setup-ruby reads api/mise.toml, and the web workflow installs Node with mise-action. Node goes from 18.17 to 24.21. Node 18 is no longer supported, and Storybook 10 needs Node 20.19 or later.
Storybook 10 removes the essentials addons and moves the test and preview-api packages into storybook. The addon now takes a createClient function, so preview.ts makes the client with the app cache. Stories now pass fn() spies in args, because actions from argTypes are no longer spies. main.ts no longer imports lodash, because Storybook 10 loads it as ESM. typescript is a dev dependency, because the import lint rule reads the storybook/test types.
The article lists used Relay cursor connections, so a client could only go to the next or previous page. The RealWorld frontend spec shows numbered pages (/?page=2). The articles, feed, user articles, and favorite articles fields now take limit and offset arguments, the same as the RealWorld API, and return the articles of the page with the total count. The *Connection fields are removed. The web app is their only client and changes in the same branch.
The article inputs took the IDs of existing tags, so a user could not add a new tag. The RealWorld API takes tag names and creates the tags that do not exist. createArticle and updateArticle now take tagList with the same behavior. Blank and duplicate names are ignored, and an empty list removes all tags.
Update Ruby in the Gemfile and mise.toml, and update all gems with bundle update. Apply the bin/rails app:update changes that the app needs, and keep the app configuration such as CORS, port 4000, Spring and the evented file watcher. Set config.load_defaults to 8.1 and remove the obsolete Rails 6.1 framework defaults file. Add the Active Storage migration that app:update copied. Disable the image variant processor because the app does not attach files. Use the generic arm64-darwin platform in Gemfile.lock and replace the deprecated mingw platforms with windows.
The JWT code read secret_key_base from the credentials file. When the master key is not available, that value is nil, so tokens were signed with an empty key in development and test. Rails.application.secret_key_base reads SECRET_KEY_BASE, then the credentials, and uses a local secret in development and test.
Set Postgres to 18.6 in mise.toml and use the postgres:18 service image in CI. Rails 8.1 and pg 1.7 support Postgres 18. Pin actions/checkout and ruby/setup-ruby to commit SHAs with a version comment, as the web workflow does. Remove the libpq-dev install step because the pg gem now ships a precompiled build for x86_64-linux.
Add the empty lines that the layout cops require and remove a redundant constant base. Give the two boolean GraphQL resolvers a question mark name with resolver_method, so the schema does not change. Allow bang methods such as User#authenticate! to return a boolean.
popularTags returned only the 20 most used tags, so a new tag did not show in the sidebar. The RealWorld frontend spec shows the list of all tags. popularTags now returns every tag that has articles, most used first and then by name, so the order is stable.
Each REST endpoint of the RealWorld API now has one GraphQL field with the same name, arguments, and data. For example, GET /api/articles is articles(tag, author, favorited, limit, offset), which returns articles and articlesCount. The types have the fields of the REST JSON: User has email, token, username, bio, and image, Profile has following, and Article has tagList, favorited, and author. Errors use the codes of the REST status codes: UNAUTHENTICATED (401), FORBIDDEN (403), NOT_FOUND (404), and UNPROCESSABLE_ENTITY (422), which has the REST errors object. An unknown article or profile is null. Follow and favorite have no effect when repeated, and an update that changes the title changes the slug. This replaces the earlier pagination and tag list fields and the permission fields, because the RealWorld API has none.
Batch the article and profile fields with GraphQL::Dataloader, so a list uses the same number of queries for any length. Add the missing indexes, cap offset, and limit query depth and complexity. Replace Devise with has_secure_password, and merge profiles into users so a user has bio and image like the RealWorld API. Derive the JWT key from the key generator, require HS256, and accept Token or Bearer headers. Return 400 for variables that are not valid JSON. The comments query returns null for an unknown article, like article and profile. Remove Spring, the unused Rails frameworks and their tables, friendly_id leftovers, and stale config and docs. CI runs RuboCop and checks that schema.graphql is current.
Replace Next.js with a client-side app, because the app has no server rendering needs. Loaders and actions own data loading and mutations, and reach Apollo Client only through the router context. Middleware loads the viewer and guards routes. Components get data as props. Follow the RealWorld frontend spec for templates, routes, and styles, with offset and limit pagination. Use the GraphQL Codegen setup that Apollo Client recommends, with typed gql documents and generated types that are not committed. Use pnpm, oxlint, and oxfmt.
Vendor the shared e2e suite from realworld-apps/realworld and run it in fullstack mode against the API on port 4000. The suite includes two fixes that are also proposed upstream: the comment sign-in locator and the logout helper race.
Stories render components and pages in a memory router with a mocked Apollo client from storybook-addon-apollo-client. Vitest runs them as browser tests with an accessibility check, and runs unit tests for the helpers in src/lib.
Describe the boundaries between routes, the app layer, components, and helpers, and how to write GraphQL documents and stories.
Run each workflow on pushes to main and on pull requests that change its project. The web workflow checks lint, format, types, tests, and the build, and runs the e2e suite against the API. Group Dependabot updates and add GitHub Actions updates.
Split src/components into ui (primitives with no domain types), layout (page structure), and features (domain components with their fragments). The primitives give one place for button styles, labeled form fields, and tabs, so forms no longer repeat the page wrapper or the field markup. Use React Router's href to build URLs. The route paths are absolute and registered for href, so a wrong path or param is a type error. A unit test checks that the router has the registered paths. Replace the pathless guard routes with signed-in and guest layout routes. The signed-in layout gives loaders, actions, and pages a user that is not null, and the guest layout holds the shared auth page layout.
Use the React Router Vite plugin with ssr: false. Routes come from src/routes.ts, src/root.tsx replaces index.html and main.tsx, and route modules use clientLoader, clientAction, and clientMiddleware. Typegen gives each route typed params, loaderData, and actionData, and registers the paths for href, so the hand-written page list and its test are gone. Pass route params straight to GraphQL variables where the names match, and split the editor into new and edit article routes. Set page titles with meta exports. Page stories build their router from src/routes.ts with createRoutesStub. Router updates do not use transitions, so a new page shows together with its URL.
The API cleanup removed config/database.yml.github-actions, so the e2e job now sets DATABASE_URL. It also starts the API in the background and waits for /up, instead of the daemon flag.
The Exclude list replaced the defaults, so RuboCop also inspected vendor/bundle, where CI installs the gems. Merge the list with the defaults instead.
In CI, bin/ci runs bin/setup with RAILS_ENV=test, and db:prepare seeds the new test database. The specs need an empty database, so the seeds skip the test environment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1861.
The API moves to Ruby 4, Rails 8.1, and Postgres 18. The GraphQL schema now maps one to one to the RealWorld REST API. Devise is replaced with has_secure_password, profiles merge into users, and lists batch their queries with the dataloader.
The web app replaces Next.js with Vite and React Router 8 in SPA mode, with Apollo Client 4 behind route loaders and actions, and Storybook 10. It follows the RealWorld frontend spec and runs the shared RealWorld e2e suite in CI.
Notes for review: