How to duplicate an object in 2.8 via the Python API
Make copies of ID data with ID.copy()

For blender data ID objects ie objects in bpy.data.objects meshes in bpy.data.meshes actions in bpy.data.actions  the ID object has a copy method .    

For a bpy.types.Object object copy, the copy will have the same transforms, parent, modifiers, constraints, animation data et all of the original.  All linked data will be same as original. eg if mesh of original is bpy.data.meshes["Cube"] so is the copy's.  Fortunately the mesh also has a copy method.

template_ob = bpy.data.objects.get("template")
if template_ob:
    ob = template_ob.copy()
    # link to collection if need be
    collection.objects.link(ob)


the copy ob has the same linked data as the original template_ob.data is ob.data is True. 

    # assign a copy of the mesh to copy object
    ob.data = ob.data.copy()


Assuming we've checked original has an action then to make the action a copy of original's

    action = ob.animation_data.action
    # make it a copy
    ob.animation_data.action = action.copy()


To unassign

    #make it None (no action assigned)
    ob.animation_data.action = None


Or in one fell swoop, remove all animation data including actions and drivers

    ob.animation_data_clear()


Constraints and modifiers data are not linked. The copy has the same of each, including the same names, as original.  The vertex groups and target objects pointed to will be same as original, and may need to be adjusted.

To remove all modifiers and constraints on copy

    ob.modifiers.clear()
    ob.constraints.clear()


Duplicate Object operator. bpy.ops.object.duplicate()

can also use the duplicate operator.  As with operators they work on the context and selected objects, which if need be can be set within a script. 

Alternatively with an override dictionary. Early testing and 2.8 appears much simpler on this front.

Test code (can confirm works as expected when run in python console on 2.8) 

Duplicate an object template_ob with linked data

bpy.ops.object.duplicate(
        {"object" : template_ob,
         "selected_objects" : [template_ob]},
        linked=True)
ob = context.object # newly duped object


linked=False is the default.

Making a new object with copies of originals data.

From question:


template_object = bpy.data.objects['TemplateObjectName']
new_object = bpy.data.objects.new('NewObjectName', template_object.data)
collection.objects.link(new_object)

  
  how do I attach the template object's animation, modifiers, particle
  systems...?


Strictly speaking this is not a duplicate. It is a new object that shares the originals data part.  Or similarly in your answer where it shares data part, and original action or copy thereof. Results will match in the simplest of cases,   ..and may be the answer you are seeking. However for others wishing to duplicate their objects in 2.8 using this method

Can they be assured that nothing is affected by not having  a property of original on the new object?


The action or driver may refer to data paths of properties on original that either,
will not be defined, or set to default on the new object.  
Relations: modifiers constraints animations could require  the original having a certain parent or transform.
How to go about copying over the constraints / modifiers of the
original? What if they have drivers? Should target objects be duped,
Vertex groups? .


To reiterate, making a new object sharing some chosen DNA of the original is IMO not duplicating.  Obviously there's no need to write a script with all the options required to emulate ob.copy()  just like there's no need to use ob.copy() to add a new object with default settings, and only a few chosen attributes of ob

Putting the body of Porsche on your new car, and driving it like it's a Porsche, doesn't make it a Porsche, it may however achieve the result you are after