$30 off During Our Annual Pro Sale. View Details »

Making an opinionated Web framework

Masaki Hara
October 26, 2019

Making an opinionated Web framework

Some Web frameworks such as Ruby on Rails intentionally force a specific software architecture and, as a result, called opinionated frameworks. Opinionated frameworks have several advantages: a standardized directory layout helps newbies to understand the codebase. They are also designed to guide programmers to adapt the program to the corresponding software architecture. So the codebase will be more tolerant of rapid growth. I think that such an opinionated Web framework written in Rust would accelerate Rust adoption in the Web programming area. I'm attempting to make it by myself, using existing Web frameworks as a reference. In this talk, I'd like to describe how I thought and learned during design and implementation.

Masaki Hara

October 26, 2019
Tweet

More Decks by Masaki Hara

Other Decks in Programming

Transcript

  1. 2019/10/26 “Making an opinionated Web framework” 1
    Making an opinionated
    Web framework
    Masaki Hara (Software Engineer at Wantedly)
    2019/10/26 @ Rust.Tokyo 2019

    View Slide

  2. 2019/10/26 “Making an opinionated Web framework” 2
    Myself
    • Software Engineer at Wantedly
    • Ruby on Rails and Golang
    • Wantedly People (Business card
    scanner & SNS)
    • Current interest: Rust
    • Former interests: formal methods,
    competitive programming, math, ...

    View Slide

  3. 2019/10/26 “Making an opinionated Web framework” 3
    Part I:
    Before designing WAF
    • Background: my Rust interests
    • Background: my jobs
    • Single-feature servers
    • Domain servers
    • Why WAF now?
    • Our prototype
    Briefly describes why I
    started building WAF.

    View Slide

  4. 2019/10/26 “Making an opinionated Web framework” 4
    Part II:
    Implementation
    • Example app: “conduit”
    • Async ecosystems
    • Structure
    • Nightly
    • Derive macros
    • Basic HTTP Tools
    • Diesel async wrapper
    • Overall progress
    Shows how our (partial)
    implementation is going.

    View Slide

  5. 2019/10/26 “Making an opinionated Web framework” 5
    Part III:
    Design Questions
    • Schema-first development
    • Error handling
    • Configuration and DI
    • Crate splitting
    • Other questions raised
    Enumerates questions
    brought up during WAF
    design and our preliminary
    decisions.

    View Slide

  6. 2019/10/26 “Making an opinionated Web framework” 6
    Part I:
    Before designing WAF

    View Slide

  7. 2019/10/26 “Making an opinionated Web framework” 7
    Background: My Rust Interests
    • Language specs, features, and tricks
    Part I > Background

    View Slide

  8. 2019/10/26 “Making an opinionated Web framework” 8
    Background: My Jobs
    • Backend: microservices written mostly in Rails and Golang
    • Performance is important but not critical
    Part I > Background

    View Slide

  9. 2019/10/26 “Making an opinionated Web framework” 9
    Background: My Jobs
    • How can Rust perform better in our purpose?
    Part I > Background

    View Slide

  10. 2019/10/26 “Making an opinionated Web framework” 10
    Single-feature servers
    • Very simple tasks that can be extracted to a microservice
    “furiganer”
    “原 将己”
    “はら まさき”
    Part I > Single-feature servers

    View Slide

  11. 2019/10/26 “Making an opinionated Web framework” 11
    Single-feature servers
    • We already have one in Rust
    “refine-image-rust”
    S3
    A bit context here: our service allows user to scan business cards.
    Part I > Single-feature servers

    View Slide

  12. 2019/10/26 “Making an opinionated Web framework” 12
    Domain servers
    • A server with a single interest and multiple operations
    “users-rails”
    Register
    Get user info
    List users
    Update user info
    Part I > Domain servers

    View Slide

  13. 2019/10/26 “Making an opinionated Web framework” 13
    A pair of domain servers
    • Servers to deliver a timeline of SNS contents
    “yashima-updates” “yashima-updates-
    contents”
    Each content
    A timeline
    A bit context here: our service has SNS feature based on the users’ business cards.
    Part I > Domain servers

    View Slide

  14. 2019/10/26 “Making an opinionated Web framework” 14
    A pair of domain servers
    • Servers to deliver a timeline of SNS contents
    “yashima-updates” “yashima-updates-
    contents”
    Each content
    A timeline
    Fast simple delivery
    Easy implementation
    of complex business
    logics
    Part I > Domain servers

    View Slide

  15. 2019/10/26 “Making an opinionated Web framework” 15
    A pair of domain servers
    • This split was well-designed and is working well.
    • However, there’s still a pain in the split; we often
    have business requirements which spans both
    servers.
    • What if there’s a language which is fast enough,
    scales well, and can handle complex business logics
    with reusable combinators…?
    Part I > Domain servers

    View Slide

  16. 2019/10/26 “Making an opinionated Web framework” 16
    A pair of domain servers
    • This split was well-designed and is working well.
    • However, there’s still a pain in the split; we often
    have business requirements which spans both
    servers.
    • What is there’s a language which is fast enough,
    scales well, and can handle complex business logics
    with reusable combinators…?
    There is, but we need the ecosystem
    Part I > Domain servers

    View Slide

  17. 2019/10/26 “Making an opinionated Web framework” 17
    Then, why WAF now?
    •…because we have the largest
    turning point in the Rust
    webserver ecosystem.
    Part I > Why WAF now?

    View Slide

  18. 2019/10/26 “Making an opinionated Web framework” 18
    Quote from diesel async issue…
    • On 2018-01-13, @sgrif said…
    “Neither tokio nor futures have stable APIs at the
    moment”
    “It's virtually impossible to have anything that isn't
    'static.”
    The lack of borrowing awaits has been blocking the
    whole async ecosystem!
    Part I > Why WAF now?

    View Slide

  19. 2019/10/26 “Making an opinionated Web framework” 19
    We’ll experience a drastic development
    • async/await isn’t just a syntax sugar; this is the
    promising API for borrowing awaits, which didn’t
    exist before.
    • Many libraries can start designing their async API
    without sacrificing borrowing.
    • Competitions will be invoked by async/await release,
    and we may have “de facto standard” libraries for
    each genre.
    Part I > Why WAF now?

    View Slide

  20. 2019/10/26 “Making an opinionated Web framework” 20
    Our prototype
    •See https://github.com/qnighy/nails
    •We’ll describe the progress in the next part
    Part I > Our prototype

    View Slide

  21. 2019/10/26 “Making an opinionated Web framework” 21
    Part II:
    Implementation

    View Slide

  22. 2019/10/26 “Making an opinionated Web framework” 22
    Example app: “conduit”
    • Same API, multiple implementations
    • Definitely useful for designing WAFs
    https://github.com/gothinkster/realworld
    Part II > Example app “conduit”

    View Slide

  23. 2019/10/26 “Making an opinionated Web framework” 23
    Async ecosystems
    tokio
    • The battle-tested runtime
    and ecosystems
    rustasync + romio + juliex
    • New runtime and
    ecosystems made by the
    official working group
    Part II > Async ecosystems

    View Slide

  24. 2019/10/26 “Making an opinionated Web framework” 24
    Async ecosystems
    tokio
    • The battle-tested runtime
    and ecosystems
    rustasync + romio + juliex
    • New runtime and
    ecosystems made by the
    official working group
    We once chose it (with
    some missing parts from
    tokio/hyper)
    Part II > Async ecosystems

    View Slide

  25. 2019/10/26 “Making an opinionated Web framework” 25
    Rustasync fate
    • https://blog.rust-lang.org/2019/09/30/Async-await-hits-
    beta.html
    Part II > Async ecosystems

    View Slide

  26. 2019/10/26 “Making an opinionated Web framework” 26
    Structure
    .
    lib
    nails
    nails_derive
    project_examples
    nails_realworld
    Cargo.toml
    Frameworks
    “Realworld” example
    Workspace manifest
    Part II > Structure

    View Slide

  27. 2019/10/26 “Making an opinionated Web framework” 27
    Nightly for async/await
    • Pinned to the latest nightly with rustfmt, clippy, and rls.
    • Moved to 1.39.0 beta
    Part II > Nightly for async/await

    View Slide

  28. 2019/10/26 “Making an opinionated Web framework” 28
    Derive macros
    • High priority because it’s tied to the core developer
    experience
    Part II > Derive macros

    View Slide

  29. 2019/10/26 “Making an opinionated Web framework” 29
    Basic HTTP Tools
    These were necessary in the early stage of prototyping:
    • Routing
    • Parsing query strings
    • Parsing JSON bodies
    Part II > Basic HTTP Tools

    View Slide

  30. 2019/10/26 “Making an opinionated Web framework” 30
    Diesel async wrapper
    • Diesel is a (supposedly most famous) type-
    safe query builder and ORM
    • Doesn’t support async yet
    • We’ll need the async wrapper eventually, but
    not for the prototyping
    Part II > Diesel async wrapper

    View Slide

  31. 2019/10/26 “Making an opinionated Web framework” 31
    Overall progress
    • Not good ;(
    • ☑️ A derive macro for parsing requests
    • ☑️ Basic routing
    • ☑️ Error handling
    • ☑️ Async/await
    • ☐ Schema generation
    • ☐ Better ORM integration
    • ☐ Working webapp example
    • ☐ Middleware interface
    • ☐ Generators / scaffolding
    • ☐ Polish polish and polish…
    Part II > Overall progress

    View Slide

  32. 2019/10/26 “Making an opinionated Web framework” 32
    Part III:
    Design Questions

    View Slide

  33. 2019/10/26 “Making an opinionated Web framework” 33
    In this part…
    • As I said in the last page, our framework itself is totally under
    development.
    • However, we’re sure we already had good design questions
    and partial answers to these.
    • In this part, we focus on questions, rather than how I solved.
    Part III > In this part…

    View Slide

  34. 2019/10/26 “Making an opinionated Web framework” 34
    Schema-first development
    • In a statically-typed language, we can give each endpoint a
    type.
    • We want to help clients (e.g. JS frontend, Android, iOS, …)
    build correct requests using these types.
    Part III > Schema-first

    View Slide

  35. 2019/10/26 “Making an opinionated Web framework” 35
    Case I: gRPC and grpc-gateway
    • gRPC by itself is a schemaful solution, but it can also be
    transformed to a schemaful JSON sever using grpc-gateway.
    From
    https://github.com/grpc-
    ecosystem/grpc-gateway
    Part III > Schema-first

    View Slide

  36. 2019/10/26 “Making an opinionated Web framework” 36
    Case I: gRPC and grpc-gateway
    “yashima-updates” “yashima-updates-
    contents”
    Each content
    A timeline
    This is grpc-gateway
    They interact using protobuf
    Part III > Schema-first

    View Slide

  37. 2019/10/26 “Making an opinionated Web framework” 37
    Case I: gRPC and grpc-gateway
    • Protocol example
    Part III > Schema-first

    View Slide

  38. 2019/10/26 “Making an opinionated Web framework” 38
    Case I: gRPC and grpc-gateway
    • Protocol example
    Part III > Schema-first

    View Slide

  39. 2019/10/26 “Making an opinionated Web framework” 39
    Case I: gRPC and grpc-gateway
    • Generated codes
    Part III > Schema-first

    View Slide

  40. 2019/10/26 “Making an opinionated Web framework” 40
    Case I: gRPC and grpc-gateway
    • Generated JSON schema (OpenAPI/Swagger)
    Part III > Schema-first

    View Slide

  41. 2019/10/26 “Making an opinionated Web framework” 41
    Case I: gRPC and grpc-gateway
    • There are several implementations in Rust too
    https://github.com/hyperium/tonic
    https://github.com/stepancheg/grpc-rust
    https://github.com/tikv/grpc-rs
    Part III > Schema-first

    View Slide

  42. 2019/10/26 “Making an opinionated Web framework” 42
    Case II: GraphQL
    • See this slide from my colleague ☺
    https://speakerdeck.com/chloe463/graphqlsabafalsesukimahuasutokai-fa-woban-nian-jing-te
    Part III > Schema-first

    View Slide

  43. 2019/10/26 “Making an opinionated Web framework” 43
    Case II: GraphQL
    • Famous Rust implementation
    https://github.com/graphql-rust/juniper
    Part III > Schema-first

    View Slide

  44. 2019/10/26 “Making an opinionated Web framework” 44
    Case III: NestJS / FastAPI
    • Generate schemas from type annotations!
    Taken from https://fastapi.tiangolo.com/tutorial/body/
    Part III > Schema-first

    View Slide

  45. 2019/10/26 “Making an opinionated Web framework” 45
    Two directions
    • In our terms, there are “Rust-first” or “Strictly schema-first”
    approaches.
    grpc-gateway GraphQL / Apollo FastAPI
    protobuf
    Go
    Ruby
    OpenAPI
    GraphQL TypeScript Python OpenAPI
    Part III > Schema-first

    View Slide

  46. 2019/10/26 “Making an opinionated Web framework” 46
    Schemas: our decision
    • Follow NestJS and FastAPI way: from Rust type to OpenAPI.
    • We can augment it with gRPC or GraphQL later.
    Part III > Schema-first

    View Slide

  47. 2019/10/26 “Making an opinionated Web framework” 47
    Error handling
    • We always use Result, but for E we have several
    choices.
    Trait Objects priv enum pub enum
    Box
    failure::Error
    pub enum MyError {
    IoError(…),

    }
    tokio::timer::Error
    hyper::error::Error
    Part III > Error-handling

    View Slide

  48. 2019/10/26 “Making an opinionated Web framework” 48
    Error handling: extra requirements
    •A framework must be able to turn errors
    into responses.
    •A framework usually has a default set of
    errors, but users may also want to define
    application-level errors.
    Part III > Error-handling

    View Slide

  49. 2019/10/26 “Making an opinionated Web framework” 49
    Case I: actix-web
    • ResponseError trait does Web-specific
    things.
    • actix_web::Error works as BoxResponseError>.

    View Slide

  50. 2019/10/26 “Making an opinionated Web framework” 50
    Case II: gotham
    • As of today, gotham::error is an alias for
    failure.
    • This is a good starting point before deciding
    how we finally handle errors.

    View Slide

  51. 2019/10/26 “Making an opinionated Web framework” 51
    Errors and schemas
    • On one hand, we want to treat errors
    uniformly.
    • On the other hand, we want to describe errors
    precisely in the generated schemas. Different
    handlers return different errors.
    • Most frameworks sacrifice error preciseness
    (which seems basically rational)

    View Slide

  52. 2019/10/26 “Making an opinionated Web framework” 52
    Error handling: our decision
    • A predefined enum NailsError, which is extendable by a
    trait ServiceError
    • Like trait objects, applications can arbitrarily add new errors.
    • An application is free to construct/inspect framework-
    defined errors.

    View Slide

  53. 2019/10/26 “Making an opinionated Web framework” 53
    Error handling: our decision
    • Decouple error data and formatter using these interfaces:
    • Display implementation (private error message)
    • fn public_message(&self) (public error message)
    • fn class_name(&self) (error classification)
    • fn status(&self) (HTTP status code)
    • A formatter turns errors into responses.
    • Extract JSON schema from the formatter.
    • Optional runtime verification of error classes for better
    error description in the generated schema

    View Slide

  54. 2019/10/26 “Making an opinionated Web framework” 54
    Configuration and DI
    •Configuration is something like
    DATABASE_URL, POOL_SIZE, or an actual
    pool of connections.
    •DI (Dependency Injection) is a
    replaceable/mockable component passed
    as a parameter.

    View Slide

  55. 2019/10/26 “Making an opinionated Web framework” 55
    Configuration and DI
    •Configuration and DI are similar in that
    they’re globally shared parameters for the
    application.
    •I call them contexts in this presentation

    View Slide

  56. 2019/10/26 “Making an opinionated Web framework” 56
    Configuration and DI
    •Due to its shared nature, it’s expected to
    be a (shallow-)cloneable object.
    •Configuration is a struct
    Arc
    •and DI is a trait object
    Arc

    View Slide

  57. 2019/10/26 “Making an opinionated Web framework” 57
    Typed vs. Untyped contexts
    Typed Contexts
    • Handler
    • Router
    • Middleware
    • Ctx is application-defined
    Untyped Contexts
    • Handler
    • Router
    • Middleware
    • We extract an application-
    defined Ctx from WAF-
    defined AnyCtx

    View Slide

  58. 2019/10/26 “Making an opinionated Web framework” 58
    Cases: actix-web 0.7.x vs. 1.x
    actix-web 0.7.x
    • App
    • HttpRequest
    • FromRequest
    • Middleware
    • State is the context
    extractor
    actix-web 1.x
    • App
    • HttpRequest
    • FromRequest
    • Transform (S isn’t a state)
    • Data is the context
    extractor (no connection with T in
    App)

    View Slide

  59. 2019/10/26 “Making an opinionated Web framework” 59
    Really typed DI
    • Each injected dependency can have its own type parameter.
    • MyAppA: AuthorizationProvider>
    • I think for most cases, this is too verbose with relatively little
    performance and safety benefits.

    View Slide

  60. 2019/10/26 “Making an opinionated Web framework” 60
    App and request contexts
    • Context may have states (with interior
    mutability). Mocked dependency usually has
    states.
    • Application-global states and request-local
    states are different. Instrumentations may want
    to store their statistics in both states.
    • How do we distinguish them?

    View Slide

  61. 2019/10/26 “Making an opinionated Web framework” 61
    Context typedness: our decision
    • Use typed contexts.
    • Each application defines AppCtx and ReqCtx.
    • AppCtx + WAF’s request data → ReqCtx

    View Slide

  62. 2019/10/26 “Making an opinionated Web framework” 62
    Context conversion: AsRef and Into
    • In a typed contexts setting, we sometimes need to convert
    between different contexts for better compositionality.
    • Question: AsRef or Into, which is better?

    View Slide

  63. 2019/10/26 “Making an opinionated Web framework” 63
    Approach: AsContext
    • Similar to Into but clone-on-write.
    • Assume contexts to be always Clone.
    pub trait Context: std::fmt::Debug + Clone {}
    pub trait AsContext: Context {
    fn as_context(&self) -> Cow<'_, U>;
    }
    impl AsContext for T {
    fn as_context(&self) -> Cow<'_, T> {
    Cow::Borrowed(self)
    }
    }

    View Slide

  64. 2019/10/26 “Making an opinionated Web framework” 64
    Approach: AsContext
    • We only pay costs on context type boundary.
    • As flexible as Into.

    View Slide

  65. 2019/10/26 “Making an opinionated Web framework” 65
    Crate splitting
    • We want to implement boilerplates (or generators) in the
    near future (but not now)
    • Directory structure is an important aspect of opinionated
    frameworks.

    View Slide

  66. 2019/10/26 “Making an opinionated Web framework” 66
    Layered architecture variants
    • There are several variants of layered
    architecture.
    • They’re common in that they enforce
    one-way dependency of layers.
    Persistence
    Domain Model
    Application
    Presentation

    View Slide

  67. 2019/10/26 “Making an opinionated Web framework” 67
    1 layer = 1 crate?
    • In Rust world, one-way dependency is considered a good
    opportunity of crate splitting, but...
    app_presentation
    src
    Cargo.toml
    app_domain_service
    src
    Cargo.toml
    app_domain_model
    src
    Cargo.toml
    Isn’t this too verbose as a starting point?

    View Slide

  68. 2019/10/26 “Making an opinionated Web framework” 68
    1 layer = 1 crate?
    • In Rust world, one-way dependency is considered a good
    opportunity of crate splitting, but...
    src
    app_application
    Cargo.toml
    app_domain_service
    Cargo.toml
    app_domain_model
    Cargo.toml This requires extra stanza in Cargo.toml

    View Slide

  69. 2019/10/26 “Making an opinionated Web framework” 69
    1 layer = 1 crate?
    • In Rust world, one-way dependency is considered a good
    opportunity of crate splitting, but...
    src
    app_application
    Cargo.toml
    app_domain_service
    app_domain_model
    Mono-crate is concise and simple

    View Slide

  70. 2019/10/26 “Making an opinionated Web framework” 70
    Crate splitting: our decision
    • I’m yet to decide this one…
    • We also have “hybrid” approach:
    • Initially, the generator generates a mono-crate tree.
    • At some stage, the programmer opt in to the splitted structure.

    View Slide

  71. 2019/10/26 “Making an opinionated Web framework” 71
    Other questions raised
    • Generators / boilerplates / scaffolding
    • Middleware interfaces
    • Devserver and autoloading
    • Mocking
    • @asomers gave a great comparison and even made the greatest
    mocking library.

    View Slide

  72. 2019/10/26 “Making an opinionated Web framework” 72
    Summary & Recap
    • Part I: we want to apply Rust
    for business-logic-heavy Web
    backends.
    • Part II: we started
    implementing WAF but it’s still
    a prototype.
    • Part III: there are interesting
    design questions about
    opinionated WAFs.
    Thanks!

    View Slide

  73. 2019/10/26 “Making an opinionated Web framework” 73
    Recap:
    Part I:
    Before designing WAF
    • Background: my Rust interests
    • Background: my jobs
    • Single-feature servers
    • Domain servers
    • Why WAF now?
    • Our prototype
    Briefly describes why I
    started building WAF.

    View Slide

  74. 2019/10/26 “Making an opinionated Web framework” 74
    Recap:
    Part II:
    Implementation
    • Example app: “conduit”
    • Async ecosystems
    • Structure
    • Nightly
    • Derive macros
    • Basic HTTP Tools
    • Diesel async wrapper
    • Overall progress
    Shows how our (partial)
    implementation is going.

    View Slide

  75. 2019/10/26 “Making an opinionated Web framework” 75
    Recap:
    Part III:
    Design Questions
    • Schema-first development
    • Error handling
    • Configuration and DI
    • Crate splitting
    • Other questions raised
    Enumerates questions
    brought up during WAF
    design and our preliminary
    decisions.

    View Slide