- NSDateFormatter - Code
- NSDateFormatter - Site
- NSURL - Code
- NSURL - Site
- String Filter - Code
- String Filter - Site
An open source, server side Swift framework. It will allow you to write web apps and RESTful API's using the language you already know and love.
- Not many people know it (marketable)
- It's impressive (unique skill)
- You can create your own backend (!!!!!)
- You can create web apps (!)
Once you have Vapor installed on your machine, you can create a new Vapor project from a default template by running
vapor new <project-name>
This will create a new project in your current directory. Out of the box, the vapor template is just a Swift module template with libraries specifically used by Vapor (see Folder Structure). To actually start working with it inside our IDE, Xcode, we need to run one more command in the root of the project:
vapor xcode -y
Running this command will not only create the Xcode project, but it will also build the dependancies needed for our project (more on this later). This stage will take a little bit of time to perform as Vapor will be fetching dependancies. The -y
parameter will have Vapor automatically open the xcode project after building it.
With XCode open, switch your schema to use the executable App schema (denoted by a little terminal window icon) on My Mac and Build the project.
You should now be able to Run the project. Doing so will launch a local server on your Mac. To verify, go to
localhost:8080
You should see a simple message from Vapor.
And that's it for getting up your environment started!
--
We're going to be writing our code in the Sources/App/main.swift
file. Delete the boilerplate code besides
import Vapor
import HTTP // you'll have to add this import
let drop = Droplet()
drop.run()
The Droplet
object is the singleton that will be managing all of your Vapor actions.
You may have touched upon, or heard of, routes in your studies. In Swift, there is a design pattern that uses Routes to transition between view controllers. From my understanding, this comes from web architecture where routes
describe the various paths you could take when navigating a website. For example, on github.com
you would have a route that corresponds to each registered user's account:
github.com/spacedrabbit
github.com/martyav
github.com/seymotom
// `github.com` is the base url any any other page is some route from there.
Ok let's say we're making an amazing CatAPI for logging when we see cats. We'll want this API to return lists of cats that we've seen and we should be able to log new cat sightings.
We have to start somewhere, so let's start with a basic GET
to make sure that we can communicate with the site. So just after we instatiate the Droplet
, add in:
// creates a /cats route
drop.get("cats") { (request: Request) in
}
Calling get
on drop
creates a new GET
route for our API and names it /cats
. We can't compile/run just yet because the closure needs one of three things:
- It needs to
throw
- It needs to return something (a
Node
for example) - It needs to return a response (something conforming to
ResponseRepresentable
)
so let's continue with:
// creates a /cats route
drop.get("cats") { (request: Request) in
return "We've got all the cats"
}
Now if we run and go to locahost:8080/cats
we can verify that we can see our text!
More common than returning a string from an API is returning some JSON
. Vapor has a built in class, JSON
, that essentially acts as a wrapper on dictionaries (it's very similar to the JSON
de/serialization we've done). So instead of returning a single string, let's return a JSON
response:
// creates a /cats route
drop.get("cats") { (request: Request) in
// return "We've got all the cats"
return try JSON(node: ["all": ["Mittens", "Socks", "Garfield"]])
}
We mark the return with try
because the conversion to JSON
can throw (just like in our practice of serialization). This is pretty neat on its own, but it becomes more salient what is happening if we run the request through Postman. Go ahead and try that now.
Let's say we had a multitude of cats, just like we have a multitude of users. How would we specify the route needed for each one of them? Well, let's agree that our cats are registered numerically and that we can access their particular instance by going to /cats/{id}
where id
is an Int
value corresponding to their id
.
It possible to do something like
// creates /cats/1
drop.get("cats", "1") { (request: Request) in
return "The cat id is \(1)"
}
// creates /cats/2
drop.get("cats", "2") { (request: Request) in
return "The cat id is \(2)"
}
But that would take up too much time to do for each cat. So instead, we create a parameterized method call:
drop.get("cats", ":id") { (request: Request) in
guard let catId = request.parameters["id"]?.int else {
throw Abort.notFound
}
return "The cat id is \(catId)"
}
There are a number of properties on the request
object that can be checked for different bits of information that correspond to parameters/queries/headers/etc we'd pass in code when constructing our Swift URL
object. In this case, we're constructing something that will look like localhost:8080/cats/<int>
. We check the parameters
property of request
and look for a key
of id
, which corresponds to the second string parameter we passed in the get
call.
Vapor gives us another option to ensure type safety:
// in this version, you specify the parameter type and include a var for it in the closure
drop.get("cats", Int.self) { (request: Request, id: Int) in
return "The cat id is \(id)"
}
This also gives us the opportunity to look at the errors we can throw. There are a large number of built-in errors that can be utilized to indicate that a problem has occured, and Vapor allows for you to create your own custom ones as well.
drop.get("cats-error") { (request: Request) in
throw Abort.custom(status: .internalServerError, message: "Cats are tangled up in the computer cables.\nTry again later")
}
If you're thinking that we'll probably need to define a model for our theoretical Cats
object... well, I guess I taught you well :D
In order to build up to, and understand the Model
protocol, we need to begin with the NodeRepresentable
, JSONRepresentable
and ResponseRepresentable
protocols
This protocol needs two methods:
func makeNode(context: Context) throws -> Node
func makeNode() throws -> Node
What is a Node
? Glad you asked:
Node is meant to be a transitive data structure that can be used to facilitate conversions between different types.
It's an object that we can convert between Bool
, Int
, String
, Array
, etc. easily if needed/possible. Let's try conforming to this protocol.
- Create a new Swift file, named
Cats
- Selected the app's executable target for the file
- Place it in the
/Sources/App/Models
folder - Import
Vapor
andHTTP
We'll give the Cat
class three properties: name
, breed
, and preferredSnack
. Creating the Node
is straightforward since it is overloaded to accept dictionary literals:
import Foundation
import Vapor
import HTTP
class Cat: NodeRepresentable {
var name: String!
var breed: String!
var preferredSnack: String!
init(name: String, breed: String, preferredSnack: String) {
self.name = name
self.breed = breed
self.preferredSnack = preferredSnack
}
func makeNode() throws -> Node {
return try Node(node: ["name": self.name,
"breed": self.breed,
"preferredSnack": self.preferredSnack])
}
func makeNode(context: Context) throws -> Node {
return try makeNode()
}
}
Ignore the
Context
parameter for now. It's there to allow easy extensibility for other modules
And back in our main.swift
file, add in:
drop.get("cats", "mittens") { request in
return try JSON(node: Cat(name: "Mittens", breed: "American Shorthair", preferredSnack: "Chicken"))
}
Now run and check localhost:8080/cats/mittens
.
The reason we need to wrap up the Cat object in a JSON initialization is that the close expects to return an object of type ResponseRepresentable
, which JSON
conforms to but NodeRepresentable
does not.
Fortunately, we can get JSONRepresentable
for free as long as we conform to NodeConvertible
as well (an extension gives a default implementation for cases were Self: NodeRepresentable
). What does that mean for us? A tiny bit less code:
drop.get("cats", "mittens") { request in
return try Cat(name: "Mittens", breed: "American Shorthair", preferredSnack: "Chicken").makeJSON()
}
OK we're almost there!
Got a sense for naming conventions in the Vapor library by now? You should, it's all about how data can be represented that ensure the kind of guarantees we need. Each xxxxxRepresentable
ensures that the object can behave in ways that the library expects. In this case, ResponseRepresentable
ensures that we can have a Response
object, which is directly analogous to an HTTP
response we'd get in Postman or an HTTPURLResponse
object in Swift.
Here's the best part though: something that is JSONRepresentable
has a .makeResponse()
function that returns a Response
! So for our class, after saying we conform to ResponseRepresentable
, we just need to one necessary function to Cat.swift
:
func makeResponse() throws -> Response {
return try self.makeJSON().makeResponse()
}
And for our code we can simply say:
drop.get("cats", "mittens") { request in
return Cat(name: "Mittens", breed: "American Shorthair", preferredSnack: "Chicken")
}
Neat!
Great, we know how to request data back from our API, but we'll likely need to POST
some up as well. For now, we're just going to make it work and in the future we'll take a look at persistant storage.
You can inspect the Request
object for query params, header info, body info etc.. the way this is tested is via Postman (just be sure to set breakpoints ahead of time before you build/run). In our case, we're going to pass in some values via a Request
's httpBody
and check for their existance in our Droplet
closure.
Setting up a POST
endpoint is similar to GET
and we can test the results via Postman:
drop.post("cats") { (request: Request) in
return "You're posting cats"
}
Now that making the request works, let's start unpacking data by first checking to see if in the JSON
body that was passed in the request has a key named name
:
drop.post("cats") { (request: Request) in
guard let catName = request.json?["name"]?.string else {
throw Abort.badRequest
}
return "Cat name was: \(catName)"
}
Assuming we want to create a fully-fledged Cat
that meets our model's needs, let's pass more key-value pairs in the body and look for them:
drop.post("cats") { (request: Request) in
guard let catName = request.json?["name"]?.string,
let catBreed = request.json?["breed"]?.string,
let snack = request.json?["preferredSnack"]?.string
else {
throw Abort.badRequest
}
let newCat = Cat(name: catName, breed: catBreed, preferredSnack: snack)
let responseCat = try newCat.makeResponse()
print("Cat successfully posted: \(responseCat)")
return responseCat
}
Neat!!
- Redirects
- Groups
- Validation
- Leaf
- Dependencies
- Database Options
- CRUD Actions
- Deploying